Sync email threads from IMAP to Markdown, draft replies, push routing intelligence to Cloudflare
Project description
Correspondence Kit
Alpha software. Expect breaking changes between minor versions. See VERSIONS.md for migration notes.
Consolidate conversations from multiple email accounts into a single flat directory of Markdown files. Draft replies with AI assistance. Push routing intelligence to Cloudflare.
Corrkit syncs threads from any IMAP provider (Gmail, Protonmail Bridge, self-hosted) into correspondence/conversations/ — one file per thread, regardless of source. A thread that arrives via both Gmail and Protonmail merges into one file. Labels, accounts, and contacts are metadata, not directory structure. Slack and social media sources are planned.
Install
Requires Python 3.12+ and uv.
cp accounts.toml.example accounts.toml # configure your email accounts
uv sync
Account configuration
Define email accounts in accounts.toml with provider presets:
[accounts.personal]
provider = "gmail" # gmail | protonmail-bridge | imap
user = "you@gmail.com"
password_cmd = "pass email/personal" # or: password = "inline-secret"
labels = ["correspondence"]
default = true
[accounts.proton]
provider = "protonmail-bridge"
user = "you@proton.me"
password_cmd = "pass email/proton"
labels = ["private"]
[accounts.selfhosted]
provider = "imap"
imap_host = "mail.example.com"
smtp_host = "mail.example.com"
user = "user@example.com"
password_cmd = "pass email/selfhosted"
labels = ["important"]
Provider presets fill in IMAP/SMTP connection defaults:
| Field | gmail |
protonmail-bridge |
imap (generic) |
|---|---|---|---|
| imap_host | imap.gmail.com | 127.0.0.1 | (required) |
| imap_port | 993 | 1143 | 993 |
| imap_starttls | false | true | false |
| smtp_host | smtp.gmail.com | 127.0.0.1 | (required) |
| smtp_port | 465 | 1025 | 465 |
| drafts_folder | [Gmail]/Drafts | Drafts | Drafts |
Any preset value can be overridden per-account. Credential resolution: password (inline)
or password_cmd (shell command, e.g. pass email/personal).
Backward compat: If no accounts.toml exists, falls back to .env GMAIL_* vars.
Legacy .env configuration
| Variable | Required | Description |
|---|---|---|
GMAIL_USER_EMAIL |
yes | Your Gmail address |
GMAIL_APP_PASSWORD |
yes | App password |
GMAIL_SYNC_LABELS |
yes | Comma-separated Gmail labels to sync |
GMAIL_SYNC_DAYS |
no | How far back to sync (default: 3650) |
CLOUDFLARE_ACCOUNT_ID |
no | For routing intelligence push |
CLOUDFLARE_API_TOKEN |
no | For routing intelligence push |
CLOUDFLARE_D1_DATABASE_ID |
no | For routing intelligence push |
Usage
All commands are available through the corrkit CLI:
corrkit --help # Show all commands
corrkit sync # Sync all accounts
corrkit sync --account personal # Sync one account
corrkit sync --full # Full re-sync (ignore saved state)
corrkit sync-gmail # Alias for sync (backward compat)
corrkit list-folders [ACCOUNT] # List IMAP folders for an account
corrkit push-draft correspondence/drafts/FILE.md # Save a draft via IMAP
corrkit push-draft correspondence/drafts/FILE.md --send # Send via SMTP
corrkit add-label LABEL --account NAME # Add a label to an account's sync config
corrkit contact-add NAME --email EMAIL # Add a contact with context docs
corrkit for add NAME --label LABEL # Add a collaborator
corrkit for sync [NAME] # Push/pull shared submodules
corrkit for status # Check for pending changes
corrkit for remove NAME # Remove a collaborator
corrkit for rename OLD NEW # Rename a collaborator directory
corrkit for reset [NAME] # Pull, regenerate templates, commit & push
corrkit by find-unanswered # Find threads awaiting a reply
corrkit by validate-draft FILE # Validate draft markdown files
corrkit watch # Poll IMAP and sync on an interval
corrkit watch --interval 60 # Override poll interval (seconds)
corrkit audit-docs # Audit instruction files for staleness
corrkit help # Show command reference
Run with uv run corrkit <subcommand> if the package isn't installed globally.
Synced threads are written to correspondence/conversations/[slug].md (flat, one file per thread). Labels and accounts are metadata inside each file. A manifest.toml index is generated after each sync.
Development
uv run pytest # Run tests
uv run ruff check . # Lint
uv run ruff format . # Format
uv run ty check # Type check
uv run poe precommit # Run ty + ruff + tests
Unified conversation directory
All synced threads live in one flat directory:
correspondence/
conversations/ # one file per thread, all sources merged
project-update.md # immutable slug filename
lunch-plans.md # mtime = last message date (ls -t sorts by activity)
quarterly-review.md
contacts/ # per-contact context for drafting
alex/
AGENTS.md # relationship, tone, topics, notes
CLAUDE.md -> AGENTS.md
drafts/ # outgoing messages
manifest.toml # thread index (generated by sync)
No subdirectories for accounts or labels. A conversation with the same person may arrive via
Gmail, Protonmail, or both — it merges into one file. Source metadata is tracked inside each file
(**Labels**, **Accounts**) and in manifest.toml.
Immutable filenames. Each thread gets a [slug].md name derived from the subject on first write.
The filename never changes, even as new messages arrive. Thread identity is tracked by **Thread ID**
metadata inside the file.
manifest.toml indexes every thread by subject, labels, accounts, contacts, and last-updated date. Agents read the manifest for discovery, then go straight to the file for content.
Extensible to new sources. The flat model means adding Slack or social media sync doesn't change the directory layout — new messages merge into the same directory with their source tracked in metadata.
Sandboxing
Most AI email tools (OpenClaw, etc.) require OAuth access to your entire account. Once authorized, the agent can read every message, every contact, every thread — and you're trusting the service not to overreach.
Correspondence-kit inverts this. You control what any agent or collaborator can see:
- You label threads in your email client. Only threads you explicitly label get synced locally.
- Labels route to scoped views. Each collaborator/agent gets a submodule containing only the threads labeled for them — nothing else.
- Credentials never leave your machine.
accounts.tomlis gitignored. Agents draft replies in markdown; only you can push to your email.
An agent added with corrkit for add assistant --label for-assistant can only see threads you've tagged for-assistant. It can't see your other conversations, your contacts, or other collaborators' repos. If the agent is compromised, the blast radius is limited to the threads you chose to share.
This works across multiple email accounts — Gmail, Protonmail, self-hosted — each with its own labels and routing rules, all funneling through the same scoped collaborator model.
Contacts
Per-contact directories give Claude context when drafting emails — relationship history, tone preferences, recurring topics.
Adding a contact
corrkit contact-add alex --email alex@example.com --email alex@work.com --label correspondence --account personal
This creates correspondence/contacts/alex/ with an AGENTS.md template (+ CLAUDE.md symlink) and updates contacts.toml.
Contact context
Edit correspondence/contacts/{name}/AGENTS.md with:
- Relationship: How you know this person, shared history
- Tone: Communication style overrides (defaults to voice.md)
- Topics: Recurring subjects, current projects
- Notes: Freeform context — preferences, pending items, important dates
contacts.toml
Maps contacts to email addresses and conversation labels (for lookup, not sync routing):
[alex]
emails = ["alex@example.com", "alex@work.com"]
labels = ["correspondence"]
account = "personal"
Copy contacts.toml.example to contacts.toml to get started.
Collaborators
Share specific email threads with people or AI agents via scoped GitHub repos.
Adding a collaborator
# Human collaborator (invited via GitHub)
corrkit for add alex-gh --label for-alex --name "Alex"
# AI agent (uses a PAT instead of GitHub invite)
corrkit for add assistant-bot --label for-assistant --pat
# Bind all labels to one account
corrkit for add alex-gh --label for-alex --account personal
# Per-label account scoping (proton-dev account, INBOX folder)
# Use account:label syntax in collaborators.toml directly
This creates a private GitHub repo ({owner}/to-{gh-user}), initializes it with instructions, and adds it as a submodule under for/{gh-user}/. Collaborators use uvx corrkit by ... for helper commands.
Daily workflow
# 1. Sync emails -- shared labels route to for/{gh-user}/conversations/
corrkit sync
# 2. Push synced threads to collaborator repos & pull their drafts
corrkit for sync
# 3. Check what's pending without pushing
corrkit for status
# 4. Review a collaborator's draft and push it as an email draft
corrkit push-draft for/alex-gh/drafts/2026-02-19-reply.md
Unattended sync with corrkit watch
Run as a daemon to poll IMAP, sync threads, and push to shared repos automatically:
# Interactive — polls every 5 minutes (default), Ctrl-C to stop
corrkit watch
# Custom interval
corrkit watch --interval 60
Configure in accounts.toml:
[watch]
poll_interval = 300 # seconds between polls (default: 300)
notify = true # desktop alerts on new messages (default: false)
Running as a system service
Linux (systemd):
cp services/corrkit-watch.service ~/.config/systemd/user/
# Edit WorkingDirectory in the unit file to match your setup
systemctl --user enable --now corrkit-watch
journalctl --user -u corrkit-watch -f # view logs
macOS (launchd):
cp services/com.corrkit.watch.plist ~/Library/LaunchAgents/
# Edit WorkingDirectory in the plist to match your setup
launchctl load ~/Library/LaunchAgents/com.corrkit.watch.plist
tail -f /tmp/corrkit-watch.log # view logs
What collaborators can do
- Read conversations labeled for them
- Draft replies in
for/{gh-user}/drafts/following the format in AGENTS.md - Run
uvx corrkit by find-unansweredanduvx corrkit by validate-draftin their repo - Push changes to their shared repo
What only you can do
- Sync new emails (
corrkit sync) - Push synced threads to collaborator repos (
corrkit for sync) - Send emails (
corrkit push-draft --send) - Change draft Status to
sent
Removing a collaborator
corrkit for remove alex-gh
corrkit for remove alex-gh --delete-repo # also delete the GitHub repo
Designed for humans and agents
Corrkit is built around files, CLI commands, and git — interfaces that work equally well for humans and AI agents. No GUIs, no OAuth popups, no interactive prompts.
Why this works
- Everything is files. Threads are Markdown. Config is TOML. Drafts are Markdown. Humans read them in any editor; agents read and write them natively.
- CLI is the interface. Every operation is a single
corrkitcommand. Scriptable, composable, works the same whether a human or agent is at the keyboard. - Zero-install for collaborators.
uvx corrkit by find-unansweredanduvx corrkit by validate-draftwork without cloning the main repo or setting up a dev environment. - Self-documenting repos. Each shared repo ships with
AGENTS.md(full instructions),CLAUDE.md(symlink for Claude Code),voice.md, and aREADME.md. A new collaborator — human or agent — can start contributing immediately. - Templates stay current.
corrkit for resetregenerates all template files in shared repos when the tool evolves. No manual sync of instructions across collaborators.
Owner workflow
The owner can work directly or with an AI agent (Claude Code, Codex, etc.) that has full context of both the codebase and the correspondence. In a single session:
- Develop the tool — write code, run tests, commit
- Sync emails —
corrkit sync - Manage collaborators — add, reset templates, push synced threads
- Draft replies — reading threads for context, writing drafts matching the voice guidelines
- Review collaborator drafts — validate, approve, push to email
Humans and agents use the same commands. There's no separate "agent mode" — the CLI is the universal interface.
Collaborator workflow
Each collaborator — human or agent — gets a scoped git repo with:
for/{gh-user}/
AGENTS.md # Full instructions: formats, commands, status flow
CLAUDE.md # Symlink for Claude Code auto-discovery
README.md # Quick-start guide
voice.md # Writing style guidelines
conversations/ # Synced threads (read-only for the collaborator)
drafts/ # Where the collaborator writes replies
The collaborator reads conversations, drafts replies following the documented format, validates with
uvx corrkit validate-draft, and pushes. The owner reviews and sends.
Cloudflare architecture
Python handles the heavy lifting locally. Distilled intelligence is pushed to Cloudflare storage for use by a lightweight TypeScript Worker that handles email routing.
Gmail/Protonmail
↓
Python (local, uv)
- sync threads → markdown
- extract intelligence (tags, contact metadata, routing rules)
- push to Cloudflare
↓
Cloudflare D1 / KV
- contact importance scores
- thread tags / inferred topics
- routing rules
↓
Cloudflare Worker (TypeScript)
- email routing decisions using intelligence from Python
Full conversation threads stay local. Cloudflare only receives the minimal distilled signal needed for routing.
MCP alternative
Instead of pre-syncing to markdown files, Claude can access Gmail live via an MCP server during a session. Options:
- Pipedream — hosted MCP with Gmail, Calendar, Contacts (note: data passes through Pipedream)
- Local Python MCP server — run a Gmail MCP server locally for fully private live access (future)
Current approach (file sync) is preferred for privacy and offline use. MCP is worth revisiting for real-time workflows.
Future work
- Slack sync: Pull conversations from Slack channels/DMs into the flat conversations/ directory
- Social media sync: Pull DMs and threads from social platforms into conversations/
- Project setup script: Interactive
collab-initorsetupcommand that configures accounts.toml - Cloudflare routing: TypeScript Worker consuming D1/KV data pushed from Python
- Local MCP server: Live email access during Claude sessions without Pipedream
- Multi-user: Per-user credential flow when shared with another developer
AI agent instructions
Project instructions live in AGENTS.md (symlinked as CLAUDE.md). Personal overrides go in CLAUDE.local.md / AGENTS.local.md (gitignored).
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file corrkit-0.5.0.tar.gz.
File metadata
- Download URL: corrkit-0.5.0.tar.gz
- Upload date:
- Size: 87.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"CachyOS Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8868e4a4cbc2cfc50ce8a9621c8eb689e8a513fc67e5f6dc8a288583917d434
|
|
| MD5 |
708814d6b8db86b212eb998b090d33a3
|
|
| BLAKE2b-256 |
fccaec4a24db70bd6a6b4f2bd31f064806df2afe77ccb9768047b7e8b66dfa4f
|
File details
Details for the file corrkit-0.5.0-py3-none-any.whl.
File metadata
- Download URL: corrkit-0.5.0-py3-none-any.whl
- Upload date:
- Size: 49.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"CachyOS Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e01e27b4d113b7e98ee44558bb5f9f4547fe7f642a1add866f24b38faffa83c2
|
|
| MD5 |
7951c92c6eae6dece3ecb1b53f3c7fca
|
|
| BLAKE2b-256 |
d3c32827081e19f651475a0f26f8eb8ccd8691e882b7d37639c9637f14dbf639
|