- Python 92.4%
- Shell 4.1%
- Dockerfile 3.5%
Why: - A hung/unreachable davmail connection surfaced as a plain TimeoutError rather than being wrapped in urllib.error.URLError, which check_davmail_auth() didn't catch - it crashed the whole `check_davmail` cron invocation instead of logging a warning like every other connectivity failure path. What: - Add an `except OSError` (URLError's own base class) after the URLError handler, so any bare socket-level error (TimeoutError, ConnectionRefusedError, ...) is treated the same as an unreachable davmail instead of propagating. Impact: - check_davmail cron job no longer crashes when davmail is slow to respond or unreachable; still only notifies on a confirmed 401/403 auth rejection. |
||
|---|---|---|
| src | ||
| .dockerignore | ||
| .gitignore | ||
| compose_restart.sh | ||
| crontab.txt | ||
| docker-compose.yaml | ||
| Dockerfile | ||
| pyproject.toml | ||
| README.md | ||
| startup.sh | ||
| uv.lock | ||
taskwarrior_sync_server
A personal automation daemon that syncs and enriches a Taskwarrior database: expanding repeat/template tasks, creating tasks from email, exporting due tasks to calendars (khal/vdirsyncer), and sending reminders. See CLAUDE.md for the code architecture.
This repo also holds a Docker Compose stack (docker-compose.yaml) that self-hosts the whole pipeline: the sync daemon itself, mail/calendar bridging via davmail, and the Taskwarrior sync server (tss) all Taskwarrior clients (this container, your Mac, taskfront, ...) sync against.
Deploying the Docker Compose stack
Everything needed to build the image now lives in this repo (src/, pyproject.toml, uv.lock) — the image is built from a COPY of the working tree, there's no runtime git clone/git pull step. Rebuild the image whenever the code changes; compose_restart.sh does this for you (docker compose up -d --build).
Required files (not committed — provision these at the repo root before building)
None of these are tracked in git (see .gitignore); they hold real credentials or are host-specific. khal_config and vdirsyncer_config are the only two of this group already checked in.
| File | Purpose |
|---|---|
.taskrc |
Taskwarrior config for the container. Needs data.location plus sync.server.url / sync.server.client_id / sync.encryption_secret pointing at the tss service — since task-server runs with network_mode: host and tss publishes 8765:8080 to the host, that's sync.server.url=http://localhost:8765. |
.mbsyncrc |
isync/mbsync config — IMAP mailboxes to sync, reached through davmail's local IMAP proxy (localhost:1143). |
.notmuch-config |
notmuch mail-indexing config; database path should live under /root/mail. Also baked into the image at build time (Dockerfile), then overridden by the runtime bind mount. |
PMSCred.json |
Google OAuth client credentials for vdirsyncer's Google Calendar storage. |
task_config.toml |
This app's own config, mounted to ~/.config/khal/task_cal.toml. Needs [calendars] (khal group → calendar directory mapping), [notify] (platform plus the settings for whichever of ntfy/pushover/discord you use — for ntfy, [notify.ntfy] takes server, token, default.topic, and per-project topic overrides), and [tasks]. |
vdirsyncer_huantrinh1802_dot_token |
Google OAuth token file for vdirsyncer's Google Calendar storage (referenced as token_file in vdirsyncer_config). Also baked in at build time and then shadowed by nothing at runtime (there's no bind mount for it — rebuild the image if it changes). |
khal_config |
(already in the repo) khal's own config: calendars, locale, highlighting. |
vdirsyncer_config |
(already in the repo) vdirsyncer pair/storage definitions for Gmail, iCloud, and the Fonto (davmail-proxied O365) calendars. |
davmail-config/ |
Directory. davmail's persistent settings store — can start empty. |
davmail.p12 |
PKCS12 keystore davmail uses for its local TLS listeners. |
data/ |
Directory. tss's (taskchampion-sync-server) data directory. Seed this from your existing sync server's data rather than starting empty — the CLIENT_ID in docker-compose.yaml is meant to match the client ID already in use elsewhere, so this is a continuation of an existing sync series, not a fresh one. |
Docker will silently create an empty directory for any of these that's missing at container-start time if it's mounted as a single-file bind mount — which then breaks whatever expected a file there (e.g. a missing task_config.toml becomes a directory, and tomllib.load() in task/__init__.py crashes with IsADirectoryError instead of a clear "not found"). Create all of the above before the first docker compose up.
Required host directories
docker-compose.yaml also bind-mounts paths outside this repo, on the deploy host:
$HOME/.local/share/mail→/root/mail$HOME/.local/share/khal→/root/.local/share/khal
These must already exist on the host, with the calendar subdirectories khal_config/vdirsyncer_config reference.
Required pass entries
compose_restart.sh pulls secrets from pass on the host and passes them into the container as env vars, which startup.sh then seeds into the container's own pass store:
huantrinh1802@gmail.com(Gmail password)huantrinh1802@zohomail.com.au(Zoho password)google_client_secretapple_passwordfonto_o365_password
Deploying
./compose_restart.sh
This exports the secrets above from pass, tears down the running stack, rebuilds the image, and brings it back up detached.
Multi-device sync note
tss is the same Taskwarrior sync server other clients (this Mac, taskfront) already point at. Running a Taskwarrior client's own cron-driven task sync on multiple devices against the same server is known to occasionally materialize the same recurring-task occurrence twice (Taskwarrior computes recurrence generation locally per replica). task/generators.py's create_repeat_tasks() detects and deletes such duplicates rather than re-expanding them, but adding more independently-syncing replicas (this container included) increases how often that race can happen.