aboutsummaryrefslogtreecommitdiffstats
path: root/contrib
diff options
context:
space:
mode:
Diffstat (limited to 'contrib')
-rw-r--r--contrib/caldav/README.md41
-rwxr-xr-xcontrib/caldav/calcurse-caldav.py273
-rw-r--r--contrib/caldav/config.sample23
-rwxr-xr-xcontrib/caldav/hooks/post-sync34
-rwxr-xr-xcontrib/hooks/post-save38
-rwxr-xr-xcontrib/hooks/pre-load10
-rw-r--r--contrib/import/DST-et.ical11
-rw-r--r--contrib/import/DST-eu.ical11
-rw-r--r--contrib/import/February.ical11
-rw-r--r--contrib/import/Monday-first-last.ical11
-rw-r--r--contrib/import/Tuesday-Thursday.ical11
-rw-r--r--contrib/import/Wednesdays-all.ical11
-rw-r--r--contrib/setup.cfg3
-rw-r--r--contrib/vdir/README.md3
-rwxr-xr-xcontrib/vdir/calcurse-vdirsyncer2
15 files changed, 328 insertions, 165 deletions
diff --git a/contrib/caldav/README.md b/contrib/caldav/README.md
index b6f6cbf..b464dc9 100644
--- a/contrib/caldav/README.md
+++ b/contrib/caldav/README.md
@@ -17,10 +17,12 @@ Usage
-----
calcurse-caldav requires an up-to-date version of calcurse and a configuration
-file located at ~/.calcurse/caldav/config. An example configuration file can be
-found under contrib/caldav/config.sample in the calcurse source tree. You will
-also need to install *httplib2* for Python 3 using *pip* (e.g. `pip3 install
---user httplib2`) or your distribution's package manager.
+file located at $XDG_CONFIG_HOME/calcurse/caldav/config
+(~/.local/share/calcurse/caldav/config) or ~/.calcurse/caldav/config if
+~/.calcurse exists. An example configuration file can be found under
+contrib/caldav/config.sample in the calcurse source tree. You will also need to
+install *httplib2* for Python 3 using *pip* (e.g. `pip3 install --user
+httplib2`) or your distribution's package manager.
If you run calcurse-caldav for the first time, you need to provide the `--init`
argument. You can choose between the following initialization modes:
@@ -32,20 +34,21 @@ argument. You can choose between the following initialization modes:
For subsequent calcurse-caldav invocations, you don't need to specify any
additional parameters.
-You can specify a username and password for basic authentication in the
-config file. Alternatively, the password can be passed securely from another
-program (such as *pass*) via the `CALCURSE_CALDAV_PASSWORD` environment variable like
-so:
-```
-CALCURSE_CALDAV_PASSWORD=$(pass show calcurse) calcurse-caldav
-```
+Specify your HTTP Basic authentication credentials under the config file's
+`Auth` section. The most secure approach is to save your password in a CLI
+encrypted password store (_e.g.,_ [pass](https://www.passwordstore.org/)), and
+then set `PasswordCommand` to the shell command used to retrieve it.
+If security is not a priority, you may store your password in plain text
+instead.
Hooks
-----
-You can place scripts in `$HOME/.calcurse/caldav/hooks/` to trigger actions at
-certain events. To enable a hook, add a script with one of the following names
-to this directory. Also make sure the scripts are executable.
+You can place scripts in `$XDG_CONFIG_HOME/calcurse/caldav/hooks/`
+(`~/.config/calcurse/caldav/hooks`) or `~/.calcurse/caldav/hooks` if
+`~/.calcurse` exists in order to trigger actions at certain events. To enable a
+hook, add a script with one of the following names to this directory. Also make
+sure the scripts are executable.
*pre-sync*::
Executed before the data files are synchronized.
@@ -59,10 +62,12 @@ How It Works
------------
calcurse-caldav creates a so-called synchronization database at
-`~/.calcurse/caldav/sync.db` that always keeps a snapshot of the last time the
-script was executed. When running the script, it compares the objects on the
-server and the local objects with that snapshot to identify items that were
-added or deleted. It then
+`$XDG_DATA_HOME/calcurse/caldav/sync.db`
+(`~/.local/share/calcurse/caldav/sync.db`) or `~/.calcurse/caldav/sync.db` if
+`~/.calcurse` exists that always keeps a snapshot of the last time the script
+was executed. When running the script, it compares the objects on the server
+and the local objects with that snapshot to identify items that were added or
+deleted. It then
* downloads new objects from the server and imports them into calcurse,
* deletes local objects that no longer exist on the server,
diff --git a/contrib/caldav/calcurse-caldav.py b/contrib/caldav/calcurse-caldav.py
index d247f80..f9488e6 100755
--- a/contrib/caldav/calcurse-caldav.py
+++ b/contrib/caldav/calcurse-caldav.py
@@ -3,24 +3,92 @@
import argparse
import base64
import configparser
-import httplib2
import os
+import pathlib
import re
+import shlex
import subprocess
import sys
import textwrap
-import urllib.parse
import xml.etree.ElementTree as etree
+import httplib2
+
# Optional libraries for OAuth2 authentication
try:
- from oauth2client.client import OAuth2WebServerFlow, HttpAccessTokenRefreshError
- from oauth2client.file import Storage
import webbrowser
+
+ from oauth2client.client import HttpAccessTokenRefreshError, OAuth2WebServerFlow
+ from oauth2client.file import Storage
except ModuleNotFoundError:
pass
+class Config:
+ _map = {}
+
+ def __init__(self, fn):
+ self._map = {
+ 'Auth': {
+ 'Password': None,
+ 'PasswordCommand': None,
+ 'Username': None,
+ },
+ 'CustomHeaders': {},
+ 'General': {
+ 'AuthMethod': 'basic',
+ 'Binary': 'calcurse',
+ 'Debug': False,
+ 'DryRun': True,
+ 'HTTPS': True,
+ 'Hostname': None,
+ 'InsecureSSL': False,
+ 'Path': None,
+ 'SyncFilter': 'cal,todo',
+ 'Verbose': False,
+ },
+ 'OAuth2': {
+ 'ClientID': None,
+ 'ClientSecret': None,
+ 'RedirectURI': 'http://127.0.0.1',
+ 'Scope': None,
+ },
+ }
+
+ config = configparser.RawConfigParser()
+ config.optionxform = str
+ if verbose:
+ print('Loading configuration from ' + configfn + '...')
+ try:
+ config.read_file(open(fn))
+ except FileNotFoundError:
+ die('Configuration file not found: {}'.format(fn))
+
+ for sec in config.sections():
+ if sec not in self._map:
+ die('Unexpected config section: {}'.format(sec))
+
+ if not self._map[sec]:
+ # Import section with custom key-value pairs.
+ self._map[sec] = dict(config.items(sec))
+ continue
+
+ # Import section with predefined keys.
+ for key, val in config.items(sec):
+ if key not in self._map[sec]:
+ die('Unexpected config key in section {}: {}'.format(sec, key))
+ if type(self._map[sec][key]) == bool:
+ self._map[sec][key] = config.getboolean(sec, key)
+ else:
+ self._map[sec][key] = val
+
+ def section(self, section):
+ return self._map[section]
+
+ def get(self, section, key):
+ return self._map[section][key]
+
+
def msgfmt(msg, prefix=''):
lines = []
for line in msg.splitlines():
@@ -37,8 +105,10 @@ def die(msg):
def check_dir(dir):
- if not os.path.isdir(dir):
- die("invalid directory: {0}".format(dir))
+ try:
+ pathlib.Path(dir).mkdir(parents=True, exist_ok=True)
+ except FileExistsError:
+ die("{} is not a directory".format(dir))
def die_atnode(msg, node):
@@ -145,8 +215,8 @@ def calcurse_version():
def get_auth_headers():
if not username or not password:
return {}
- user_password = ('{}:{}'.format(username, password)).encode('ascii')
- user_password = base64.b64encode(user_password).decode('ascii')
+ user_password = ('{}:{}'.format(username, password)).encode('utf-8')
+ user_password = base64.b64encode(user_password).decode('utf-8')
headers = {'Authorization': 'Basic {}'.format(user_password)}
return headers
@@ -351,18 +421,21 @@ def push_object(conn, objhash):
if not headers:
return None
-
- etag = None
headerdict = dict(headers)
- if 'etag' in headerdict:
- etag = headerdict['etag']
- while not etag:
+
+ # Retrieve href from server to match server-side format. Retrieve ETag
+ # unless it can be extracted from the PUT response already.
+ ret_href, ret_etag = None, headerdict.get('etag')
+ while not ret_etag or not ret_href:
etagdict = get_etags(conn, [href])
- if etagdict:
- etag = next(iter(etagdict.values()))
- etag = etag.strip('"')
+ if not etagdict:
+ continue
+ ret_href, new_etag = next(iter(etagdict.items()))
+ # Favor ETag from PUT response to avoid race condition.
+ if not ret_etag:
+ ret_etag = new_etag
- return (urllib.parse.quote(href), etag)
+ return (ret_href, ret_etag.strip('"'))
def push_objects(objhashes, conn, syncdb, etagdict):
@@ -519,11 +592,29 @@ def run_hook(name):
nsmap = {"D": "DAV:", "C": "urn:ietf:params:xml:ns:caldav"}
# Initialize default values.
-configfn = os.path.expanduser("~/.calcurse/caldav/config")
-lockfn = os.path.expanduser("~/.calcurse/caldav/lock")
-syncdbfn = os.path.expanduser("~/.calcurse/caldav/sync.db")
-hookdir = os.path.expanduser("~/.calcurse/caldav/hooks/")
-oauth_file = os.path.expanduser("~/.calcurse/caldav/oauth2_cred")
+if os.path.isdir(os.path.expanduser("~/.calcurse")):
+ caldav_path = os.path.expanduser("~/.calcurse/caldav")
+ check_dir(caldav_path)
+
+ configfn = os.path.join(caldav_path, "config")
+ hookdir = os.path.join(caldav_path, "hooks")
+ oauth_file = os.path.join(caldav_path, "oauth2_cred")
+ lockfn = os.path.join(caldav_path, "lock")
+ syncdbfn = os.path.join(caldav_path, "sync.db")
+else:
+ xdg_config_home = os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
+ xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
+ caldav_config = os.path.join(xdg_config_home, "calcurse", "caldav")
+ caldav_data = os.path.join(xdg_data_home, "calcurse", "caldav")
+ check_dir(caldav_config)
+ check_dir(caldav_data)
+
+ configfn = os.path.join(caldav_config, "config")
+ hookdir = os.path.join(caldav_config, "hooks")
+ oauth_file = os.path.join(caldav_config, "oauth2_cred")
+
+ lockfn = os.path.join(caldav_data, "lock")
+ syncdbfn = os.path.join(caldav_data, "sync.db")
# Parse command line arguments.
parser = argparse.ArgumentParser('calcurse-caldav')
@@ -568,105 +659,57 @@ verbose = args.verbose
debug = args.debug
debug_raw = args.debug_raw
-# Read environment variables
-password = os.getenv('CALCURSE_CALDAV_PASSWORD')
-
# Read configuration.
-config = configparser.RawConfigParser()
-if verbose:
- print('Loading configuration from ' + configfn + '...')
-try:
- config.read_file(open(configfn))
-except FileNotFoundError as e:
- die('Configuration file not found: {}'.format(configfn))
-
-if config.has_option('General', 'InsecureSSL'):
- insecure_ssl = config.getboolean('General', 'InsecureSSL')
+config = Config(configfn)
+
+authmethod = config.get('General', 'AuthMethod').lower()
+calcurse = [config.get('General', 'Binary')]
+debug = debug or config.get('General', 'Debug')
+dry_run = config.get('General', 'DryRun')
+hostname = config.get('General', 'Hostname')
+https = config.get('General', 'HTTPS')
+insecure_ssl = config.get('General', 'InsecureSSL')
+path = config.get('General', 'Path')
+sync_filter = config.get('General', 'SyncFilter')
+verbose = verbose or config.get('General', 'Verbose')
+
+if os.getenv('CALCURSE_CALDAV_PASSWORD'):
+ # This approach is deprecated, but preserved for backwards compatibility
+ password = os.getenv('CALCURSE_CALDAV_PASSWORD')
+elif config.get('Auth', 'Password'):
+ password = config.get('Auth', 'Password')
+elif config.get('Auth', 'PasswordCommand'):
+ tokenized_cmd = shlex.split(config.get('Auth', 'PasswordCommand'))
+ password = subprocess.run(tokenized_cmd, capture_output=True).stdout.decode('UTF-8')
else:
- insecure_ssl = False
+ password = None
-# Read config for "HTTPS" option (default=True)
-if config.has_option('General', 'HTTPS'):
- https = config.getboolean('General', 'HTTPS')
-else:
- https = True
+username = config.get('Auth', 'Username')
-if config.has_option('General', 'Binary'):
- calcurse = [config.get('General', 'Binary')]
-else:
- calcurse = ['calcurse']
+client_id = config.get('OAuth2', 'ClientID')
+client_secret = config.get('OAuth2', 'ClientSecret')
+redirect_uri = config.get('OAuth2', 'RedirectURI')
+scope = config.get('OAuth2', 'Scope')
+
+custom_headers = config.section('CustomHeaders')
+# Append data directory to calcurse command.
if datadir:
check_dir(datadir)
calcurse += ['-D', datadir]
-if config.has_option('General', 'DryRun'):
- dry_run = config.getboolean('General', 'DryRun')
-else:
- dry_run = True
-
-if not verbose and config.has_option('General', 'Verbose'):
- verbose = config.getboolean('General', 'Verbose')
-
-if not debug and config.has_option('General', 'Debug'):
- debug = config.getboolean('General', 'Debug')
-
-if config.has_option('General', 'AuthMethod'):
- authmethod = config.get('General', 'AuthMethod').lower()
-else:
- authmethod = 'basic'
-
-if config.has_option('General', 'SyncFilter'):
- sync_filter = config.get('General', 'SyncFilter')
-
- invalid_filter_values = validate_sync_filter()
-
- if len(invalid_filter_values):
- die('Invalid value(s) in SyncFilter option: ' + ', '.join(invalid_filter_values))
-else:
- sync_filter = 'cal,todo'
-
-if config.has_option('Auth', 'UserName'):
- username = config.get('Auth', 'UserName')
-else:
- username = None
-
-if config.has_option('Auth', 'Password') and not password:
- password = config.get('Auth', 'Password')
-
-if config.has_section('CustomHeaders'):
- custom_headers = dict(config.items('CustomHeaders'))
-else:
- custom_headers = {}
-
-if config.has_option('OAuth2', 'ClientID'):
- client_id = config.get('OAuth2', 'ClientID')
-else:
- client_id = None
-
-if config.has_option('OAuth2', 'ClientSecret'):
- client_secret = config.get('OAuth2', 'ClientSecret')
-else:
- client_secret = None
-
-if config.has_option('OAuth2', 'Scope'):
- scope = config.get('OAuth2', 'Scope')
-else:
- scope = None
-
-if config.has_option('OAuth2', 'RedirectURI'):
- redirect_uri = config.get('OAuth2', 'RedirectURI')
-else:
- redirect_uri = 'http://127.0.0.1'
-
-# Change URl prefix according to HTTP/HTTPS
-if https:
- urlprefix = "https://"
-else:
- urlprefix = "http://"
-
-hostname = config.get('General', 'HostName')
-path = '/' + config.get('General', 'Path').strip('/') + '/'
+# Validate sync filter.
+invalid_filter_values = validate_sync_filter()
+if len(invalid_filter_values):
+ die('Invalid value(s) in SyncFilter option: ' + ', '.join(invalid_filter_values))
+
+# Ensure host name and path are defined and initialize *_uri.
+if not hostname:
+ die('Hostname missing in configuration.')
+if not path:
+ die('Path missing in configuration.')
+urlprefix = "https://" if https else "http://"
+path = '/{}/'.format(path.strip('/'))
hostname_uri = urlprefix + hostname
absolute_uri = hostname_uri + path
@@ -699,9 +742,7 @@ try:
# Connect to the server.
if verbose:
print('Connecting to ' + hostname + '...')
- conn = httplib2.Http()
- if insecure_ssl:
- conn.disable_ssl_certificate_validation = True
+ conn = httplib2.Http(disable_ssl_certificate_validation=insecure_ssl)
if authmethod == 'oauth2':
# Authenticate with OAuth2 and authorize HTTP object
@@ -764,7 +805,7 @@ try:
# Write the synchronization database.
save_syncdb(syncdbfn, syncdb)
- #Clear OAuth2 credentials if used
+ # Clear OAuth2 credentials if used.
if authmethod == 'oauth2':
conn.clear_credentials()
diff --git a/contrib/caldav/config.sample b/contrib/caldav/config.sample
index 4d87681..0ba8fa8 100644
--- a/contrib/caldav/config.sample
+++ b/contrib/caldav/config.sample
@@ -1,15 +1,22 @@
# If you want to synchronize calcurse with a CalDAV server using
-# calcurse-caldav, create a new directory ~/.calcurse/caldav/, copy this file
-# to ~/.calcurse/caldav/config and adjust the configuration below.
+# calcurse-caldav, create a new directory at $XDG_CONFIG_HOME/calcurse/caldav/
+# (~/.config/calcurse/caldav/) and $XDG_DATA_HOME/calcurse/caldav/
+# (~/.local/share/calcurse/caldav/) and copy this file to
+# $XDG_CONFIG_HOME/calcurse/caldav/config and adjust the configuration below.
+# Alternatively, if using ~/.calcurse, create a new directory at
+# ~/.calcurse/caldav/ and copy this file to ~/.calcurse/caldav/config and adjust
+# the configuration file below.
[General]
# Path to the calcurse binary that is used for importing/exporting items.
Binary = calcurse
-# Host name of the server that hosts CalDAV.
+# Host name of the server that hosts CalDAV. Do NOT prepend a protocol prefix,
+# such as http:// or https://. Append :<port> for a port other than 80.
Hostname = some.hostname.com
-# Path to the CalDAV calendar on the host specified above.
+# Path to the CalDAV calendar on the host specified above. This is the base
+# path following your host name in the URL.
Path = /path/to/calendar/on/the/server/
# Type of authentication to use. Must be "basic" or "oauth2"
@@ -41,11 +48,13 @@ DryRun = Yes
# Enable this if you want detailed logs written to stdout.
Verbose = Yes
-# Credentials for HTTP Basic Authentication. Leave this commented out if you do
-# not want to use authentication.
+# Credentials for HTTP Basic Authentication (if required).
+# Set `Password` to your password in plaintext (unsafe),
+# or `PasswordCommand` to a shell command that retrieves it (recommended).
#[Auth]
#Username = user
-#Password = pass
+#Password = password
+#PasswordCommand = pass baikal
# Optionally specify additional HTTP headers here.
#[CustomHeaders]
diff --git a/contrib/caldav/hooks/post-sync b/contrib/caldav/hooks/post-sync
index 1ac028c..db0059b 100755
--- a/contrib/caldav/hooks/post-sync
+++ b/contrib/caldav/hooks/post-sync
@@ -4,14 +4,32 @@
# repository, it automatically makes a commit whenever synchronizing with a
# CalDAV server.
#
-# In order to install this hook, copy this file to ~/.calcurse/caldav/hooks/.
+# In order to install this hook, copy this file to
+# $XDG_CONFIG_HOME/calcurse/caldav/hooks/ (~/.config/calcurse/caldav/hooks/) or
+# ~/.calcurse/caldav/hooks/ if using ~/.calcurse.
-cd "$HOME"/.calcurse/
+data_dir="$HOME/.calcurse"
+config_dir="$HOME/.calcurse"
-# If the data directory is under version control, create a Git commit.
-if [ -d .git -a -x "$(which git)" ]; then
- git add apts conf keys todo
- if ! git diff-index --quiet --cached HEAD; then
- git commit -m "Automatic commit by the post-sync hook"
- fi
+if [ ! -d "$data_dir" ]; then
+ data_dir="${XDG_DATA_HOME:-$HOME/.local/share}/calcurse"
+ config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/calcurse"
fi
+
+# Do not do anything when synchronizing with a CalDAV server.
+[ -f "$data_dir/caldav/lock" ] && exit
+
+# If the directory is under version control, create a Git commit.
+commit_dir() {
+ cd "$1" >/dev/null 2>&1 || return
+ shift
+ if [ -d .git ] && command -v git >/dev/null; then
+ git add "$@"
+ if ! git diff-index --quiet --cached HEAD; then
+ git commit -m "Automatic commit by the post-sync hook"
+ fi
+ fi
+}
+
+commit_dir "$data_dir" apts todo
+commit_dir "$config_dir" conf keys
diff --git a/contrib/hooks/post-save b/contrib/hooks/post-save
index bb582e2..6e3f11e 100755
--- a/contrib/hooks/post-save
+++ b/contrib/hooks/post-save
@@ -2,26 +2,42 @@
#
# This is an example hook. It does two things whenever you save the data files:
#
-# 1. Make a commit if the calcurse data directory contains a Git repository.
+# 1. Make a commit if the calcurse directories contain a Git repository.
# 2. Synchronize with a CalDAV server if calcurse-caldav is configured.
#
-# In order to install this hook, copy this file to ~/.calcurse/hooks/.
+# In order to install this hook, copy this file to
+# $XDG_CONFIG_HOME/calcurse/hooks/ (~/.config/calcurse/hooks/) or
+# ~/.calcurse/hooks/ if using ~/.calcurse.
-cd "$HOME"/.calcurse/
+data_dir="$HOME/.calcurse"
+config_dir="$HOME/.calcurse"
+
+if [ ! -d "$data_dir" ]; then
+ data_dir="${XDG_DATA_HOME:-$HOME/.local/share}/calcurse"
+ config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/calcurse"
+fi
# Do not do anything when synchronizing with a CalDAV server.
-[ -f caldav/lock ] && exit
+[ -f "$data_dir/caldav/lock" ] && exit
-# If the data directory is under version control, create a Git commit.
-if [ -d .git -a -x "$(which git)" ]; then
- git add apts conf keys todo
- if ! git diff-index --quiet --cached HEAD; then
- git commit -m "Automatic commit by the post-save hook"
+# If the directory is under version control, create a Git commit.
+commit_dir() {
+ cd "$1" >/dev/null 2>&1 || return
+ shift
+ if [ -d .git ] && command -v git >/dev/null; then
+ git add "$@"
+ if ! git diff-index --quiet --cached HEAD; then
+ git commit -m "Automatic commit by the post-save hook"
+ fi
fi
-fi
+}
+
+commit_dir "$data_dir" apts todo
+commit_dir "$config_dir" conf keys
# Optionally run the CalDAV synchronization script in the background.
-if [ -d caldav -a -x "$(which calcurse-caldav)" ]; then
+cd "$data_dir" || exit
+if [ -d caldav ] && command -v calcurse-caldav >/dev/null; then
(
date="$(date +'%b %d %H:%M:%S')"
echo "$date Running calcurse-caldav from the post-save hook..."
diff --git a/contrib/hooks/pre-load b/contrib/hooks/pre-load
index c9ab62d..d69fbde 100755
--- a/contrib/hooks/pre-load
+++ b/contrib/hooks/pre-load
@@ -3,15 +3,19 @@
# This is an example hook. It synchronizes calcurse with a CalDAV server before
# loading the data files.
#
-# In order to install this hook, copy this file to ~/.calcurse/hooks/.
+# In order to install this hook, copy this file to
+# $XDG_CONFIG_HOME/calcurse/hooks/ (~/.config/calcurse/hooks/) or
+# ~/.calcurse/hooks/ if using ~/.calcurse.
-cd "$HOME"/.calcurse/
+[ -d "$HOME/.calcurse" ] && data_dir="$HOME/.calcurse" || data_dir="${XDG_DATA_HOME:-$HOME/.local/share}/calcurse"
+
+cd "$data_dir" || exit
# Do not do anything when synchronizing with a CalDAV server.
[ -f caldav/lock ] && exit
# Run the CalDAV synchronization script in the background.
-if [ -d caldav -a -x "$(which calcurse-caldav)" ]; then
+if [ -d caldav ] && command -v calcurse-caldav >/dev/null; then
(
date="$(date +'%b %d %H:%M:%S')"
echo "$date Running calcurse-caldav from the pre-load hook..."
diff --git a/contrib/import/DST-et.ical b/contrib/import/DST-et.ical
new file mode 100644
index 0000000..116fe95
--- /dev/null
+++ b/contrib/import/DST-et.ical
@@ -0,0 +1,11 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:
+BEGIN:VEVENT
+DTSTAMP:
+UID:
+DTSTART;VALUE=DATE:20200329
+SUMMARY:Daylight Saving Time begins (EDT)
+RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
+END:VEVENT
+END:VCALENDAR
diff --git a/contrib/import/DST-eu.ical b/contrib/import/DST-eu.ical
new file mode 100644
index 0000000..e536cf0
--- /dev/null
+++ b/contrib/import/DST-eu.ical
@@ -0,0 +1,11 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:
+BEGIN:VEVENT
+DTSTAMP:
+UID:
+DTSTART;VALUE=DATE:20200329
+SUMMARY:Daylight Saving Time begins (CEST)
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
+END:VEVENT
+END:VCALENDAR
diff --git a/contrib/import/February.ical b/contrib/import/February.ical
new file mode 100644
index 0000000..15168d4
--- /dev/null
+++ b/contrib/import/February.ical
@@ -0,0 +1,11 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:
+BEGIN:VEVENT
+DTSTAMP:
+UID:
+DTSTART:20200228T080000
+SUMMARY:Penultimate day in February
+RRULE:FREQ=MONTHLY;BYMONTHDAY=-2;BYMONTH=2
+END:VEVENT
+END:VCALENDAR
diff --git a/contrib/import/Monday-first-last.ical b/contrib/import/Monday-first-last.ical
new file mode 100644
index 0000000..431bebb
--- /dev/null
+++ b/contrib/import/Monday-first-last.ical
@@ -0,0 +1,11 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:
+BEGIN:VEVENT
+DTSTAMP:
+UID:
+DTSTART:20200106T120000
+SUMMARY:First and last Monday of the month
+RRULE:FREQ=MONTHLY;BYDAY=1MO,-1MO;COUNT=10
+END:VEVENT
+END:VCALENDAR
diff --git a/contrib/import/Tuesday-Thursday.ical b/contrib/import/Tuesday-Thursday.ical
new file mode 100644
index 0000000..84c5adb
--- /dev/null
+++ b/contrib/import/Tuesday-Thursday.ical
@@ -0,0 +1,11 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:
+BEGIN:VEVENT
+DTSTAMP:
+UID:
+DTSTART:20200102T120000
+SUMMARY:Every Tuesday and Thursday
+RRULE:FREQ=WEEKLY;BYDAY=TU,TH;COUNT=10
+END:VEVENT
+END:VCALENDAR
diff --git a/contrib/import/Wednesdays-all.ical b/contrib/import/Wednesdays-all.ical
new file mode 100644
index 0000000..40730b8
--- /dev/null
+++ b/contrib/import/Wednesdays-all.ical
@@ -0,0 +1,11 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:
+BEGIN:VEVENT
+DTSTAMP:
+UID:
+DTSTART;VALUE=DATE:20200603
+SUMMARY:All Wednesdays in June and July
+RRULE:FREQ=MONTHLY;BYDAY=WE;BYMONTH=6,7;UNTIL=20220630
+END:VEVENT
+END:VCALENDAR
diff --git a/contrib/setup.cfg b/contrib/setup.cfg
new file mode 100644
index 0000000..0691fee
--- /dev/null
+++ b/contrib/setup.cfg
@@ -0,0 +1,3 @@
+[flake8]
+max-line-length = 100
+max-complexity = 16
diff --git a/contrib/vdir/README.md b/contrib/vdir/README.md
index b040d04..676b94c 100644
--- a/contrib/vdir/README.md
+++ b/contrib/vdir/README.md
@@ -39,7 +39,8 @@ destination, potentially deleting events in the destination that are no longer
present in the origin.
You can optionally specify an alternative directory for local calcurse data
-using the `-D` flag if it differs from the default `~/.calcurse`.
+using the `-D` flag if it differs from the default `$XDG_DATA_HOME/calcurse`
+(`~/.local/share/calcurse`) or `~/.calcurse`.
Integration with vdirsyncer
---------------------------
diff --git a/contrib/vdir/calcurse-vdirsyncer b/contrib/vdir/calcurse-vdirsyncer
index c5371b5..2ac849a 100755
--- a/contrib/vdir/calcurse-vdirsyncer
+++ b/contrib/vdir/calcurse-vdirsyncer
@@ -34,7 +34,7 @@ if [ "$#" -lt 1 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
usage
fi
-DATADIR="$HOME/.calcurse"
+[ -d "$HOME/.calcurse" ] && DATADIR="$HOME/.calcurse" || DATADIR="${XDG_DATA_HOME:-$HOME/.local/share}/calcurse"
VERBOSE=""
FORCE=""