ai-courier 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai_courier-0.3.2/.claude/settings.local.json +20 -0
- ai_courier-0.3.2/.gitignore +15 -0
- ai_courier-0.3.2/CHANGELOG.md +108 -0
- ai_courier-0.3.2/CLAUDE.md +91 -0
- ai_courier-0.3.2/LICENSE +21 -0
- ai_courier-0.3.2/PKG-INFO +176 -0
- ai_courier-0.3.2/README.md +150 -0
- ai_courier-0.3.2/docs/superpowers/plans/2026-04-17-courier-v0.3-pr-body.md +27 -0
- ai_courier-0.3.2/docs/superpowers/plans/2026-04-17-courier-v0.3-release-notes.md +107 -0
- ai_courier-0.3.2/docs/superpowers/plans/2026-04-17-courier-v0.3.md +2305 -0
- ai_courier-0.3.2/docs/superpowers/specs/2026-04-17-courier-v0.3-design.md +339 -0
- ai_courier-0.3.2/pyproject.toml +62 -0
- ai_courier-0.3.2/src/courier/__init__.py +3 -0
- ai_courier-0.3.2/src/courier/cal.py +504 -0
- ai_courier-0.3.2/src/courier/cli.py +628 -0
- ai_courier-0.3.2/src/courier/config.py +80 -0
- ai_courier-0.3.2/src/courier/default_rules.yaml +139 -0
- ai_courier-0.3.2/src/courier/jmap.py +1041 -0
- ai_courier-0.3.2/src/courier/server.py +649 -0
- ai_courier-0.3.2/src/courier/state.py +220 -0
- ai_courier-0.3.2/src/courier/triage.py +76 -0
- ai_courier-0.3.2/src/courier/triage_rules.py +401 -0
- ai_courier-0.3.2/tests/__init__.py +0 -0
- ai_courier-0.3.2/tests/conftest.py +48 -0
- ai_courier-0.3.2/tests/e2e/__init__.py +0 -0
- ai_courier-0.3.2/tests/e2e/conftest.py +153 -0
- ai_courier-0.3.2/tests/e2e/test_calendar_v02.py +106 -0
- ai_courier-0.3.2/tests/e2e/test_email_attachments_v02.py +102 -0
- ai_courier-0.3.2/tests/e2e/test_email_read_v02.py +46 -0
- ai_courier-0.3.2/tests/e2e/test_email_v03.py +142 -0
- ai_courier-0.3.2/tests/e2e/test_email_write_v02.py +62 -0
- ai_courier-0.3.2/tests/test_cal.py +391 -0
- ai_courier-0.3.2/tests/test_config.py +117 -0
- ai_courier-0.3.2/tests/test_jmap.py +395 -0
- ai_courier-0.3.2/tests/test_jmap_v03.py +258 -0
- ai_courier-0.3.2/tests/test_server.py +48 -0
- ai_courier-0.3.2/tests/test_state.py +148 -0
- ai_courier-0.3.2/tests/test_triage.py +279 -0
- ai_courier-0.3.2/tests/test_triage_rules.py +670 -0
- ai_courier-0.3.2/uv.lock +1353 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(git add *)",
|
|
5
|
+
"Bash(git commit -m ' *)",
|
|
6
|
+
"Bash(git checkout *)",
|
|
7
|
+
"Bash(COURIER_LIVE_TESTS=1 pytest tests/e2e/ -v --collect-only)",
|
|
8
|
+
"Bash(ruff check *)",
|
|
9
|
+
"Bash(.venv/bin/python -m pytest --version)",
|
|
10
|
+
"Bash(.venv/bin/python -c \"import pytest; print\\(pytest.__version__\\)\")",
|
|
11
|
+
"Bash(/opt/homebrew/bin/python3 -c \"import pytest; print\\(pytest.__version__, pytest.__file__\\)\")",
|
|
12
|
+
"Bash(.venv/bin/python -m pip install -e \".[dev]\")",
|
|
13
|
+
"Bash(.venv/bin/ruff check *)",
|
|
14
|
+
"Bash(COURIER_LIVE_TESTS=1 .venv/bin/python -m pytest tests/e2e/ -v --collect-only)",
|
|
15
|
+
"Bash(.venv/bin/python -m pytest tests/ --collect-only)",
|
|
16
|
+
"Bash(git commit -m 'test: add opt-in E2E harness with session fixtures *)",
|
|
17
|
+
"Bash(COURIER_LIVE_TESTS=1 .venv/bin/pytest tests/e2e/ -v --collect-only)"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Courier are documented here. Format loosely based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/); versioning follows SemVer.
|
|
5
|
+
|
|
6
|
+
## [0.3.2] — 2026-04-17
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- **PyPI package renamed from `courier-mcp` to `ai-courier`** ahead of first
|
|
10
|
+
public PyPI release. Avoids collision with the unrelated `@trycourier/courier-mcp`
|
|
11
|
+
(Courier Inc.'s notification-API MCP server on npm) and positions the name
|
|
12
|
+
for cross-provider ambition — future Gmail / Exchange / Apple bindings
|
|
13
|
+
stay inside the same package rather than being stuck with a Fastmail-
|
|
14
|
+
flavored name.
|
|
15
|
+
- Install changes from `pip install courier-mcp` → `pip install ai-courier`.
|
|
16
|
+
- Everything else unchanged: repo stays `iamdadzilla/courier`, CLI verb
|
|
17
|
+
stays `courier`, MCP server command stays `courier-mcp`, project and
|
|
18
|
+
brand stay "Courier". Only the PyPI distribution name moves.
|
|
19
|
+
|
|
20
|
+
## [0.3.1] — 2026-04-17
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **`courier test-cleanup`** — sweep stray `[courier-e2e*]` test artifacts
|
|
24
|
+
from all mailboxes to Trash. Idempotent; filters out already-trashed
|
|
25
|
+
matches. Addresses the fixture-teardown orphan problem surfaced after
|
|
26
|
+
the first live Fastmail-MCP-free sweep.
|
|
27
|
+
|
|
28
|
+
## [0.3.0] — 2026-04-17
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- **Folder filing** — `email_mailboxes` lists folders/labels with paths;
|
|
32
|
+
`email_move(ids, to)` moves to a mailbox by ID, name, or path
|
|
33
|
+
(`"Clients/Guardian"`); `email_label(ids, labels, add)` adds/removes
|
|
34
|
+
mailbox memberships without touching other memberships.
|
|
35
|
+
- **Threaded reply** — `email_reply(email_id, body, send, include_quote)`
|
|
36
|
+
composes a proper reply (In-Reply-To, References, `Re:` prefix,
|
|
37
|
+
Reply-To-or-From To: resolution); creates a draft by default.
|
|
38
|
+
- **E2E test suite** — `tests/e2e/`, opt-in via `COURIER_LIVE_TESTS=1`,
|
|
39
|
+
self-cleaning via a `Courier/Tests` mailbox and `Courier Tests`
|
|
40
|
+
calendar. New `courier test-setup` CLI command creates the sandbox.
|
|
41
|
+
- CLI verbs: `courier mailboxes`, `courier move`, `courier label`,
|
|
42
|
+
`courier reply`, `courier test-setup`.
|
|
43
|
+
- `Email.message_id` and `Email.reply_to_addrs` — extracted from JMAP.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
- `_normalize_utcdate` silently passed non-UTC offsets through, so
|
|
47
|
+
`datetime.now(tz=UTC).isoformat()` output returned empty search
|
|
48
|
+
results with no error. Now validates via `datetime.fromisoformat`
|
|
49
|
+
and rejects non-UTC offsets with `ValueError`.
|
|
50
|
+
- `create_draft` sent `inReplyTo` as a string, but RFC 8621 §4.1.3.1
|
|
51
|
+
types it as `String[]`. Fastmail rejected threaded draft creates.
|
|
52
|
+
Now wrapped in a list; parsing normalizes list-or-string-or-None
|
|
53
|
+
back to the existing `Email.in_reply_to: str | None` contract.
|
|
54
|
+
- `pyproject.toml` now sets `asyncio_default_test_loop_scope = "session"`
|
|
55
|
+
so function-scoped tests share the session-scoped fixtures' event loop.
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
- `__version__` corrected to `0.3.0` (was stale at `0.1.0` through v0.2).
|
|
59
|
+
|
|
60
|
+
### Migration
|
|
61
|
+
Agents using Courier can now retire Fastmail MCP entirely — all
|
|
62
|
+
previous filing and threaded-reply operations now have Courier
|
|
63
|
+
equivalents. See `docs/superpowers/specs/2026-04-17-courier-v0.3-design.md`
|
|
64
|
+
for the full mapping.
|
|
65
|
+
|
|
66
|
+
## [0.2.0] — 2026-04-16
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
- **Attachment support**
|
|
70
|
+
- `Attachment` dataclass + `Email.attachments` list, populated from
|
|
71
|
+
JMAP `bodyStructure` walk.
|
|
72
|
+
- `JMAPClient.download_blob()` and `upload_blob()` using the session's
|
|
73
|
+
`downloadUrl` / `uploadUrl` templates (RFC 8620 §6.2).
|
|
74
|
+
- `create_draft()` / `send_email()` accept attachments — `send_email`
|
|
75
|
+
takes `attachment_paths` and handles upload.
|
|
76
|
+
- MCP tools `email_attachments` (list) and `email_attachment_save`
|
|
77
|
+
(download to local path). `email_send` gains optional `attachment_paths`.
|
|
78
|
+
- CLI `courier attachments <email_id>` and `courier download <email_id> <dest>`.
|
|
79
|
+
- `courier read` now shows attachment list in the header.
|
|
80
|
+
|
|
81
|
+
- **Calendar invite MIME detection**
|
|
82
|
+
- `Email.has_calendar_invite` populated by walking `bodyStructure` for
|
|
83
|
+
`text/calendar` parts.
|
|
84
|
+
- New triage guard `requires_calendar_invite`.
|
|
85
|
+
- Default rule `calendar-invite-mime` (actionable, confidence 0.95).
|
|
86
|
+
|
|
87
|
+
- **Calendar event deduplication** across calendars via `(uid, recurrence_id)`.
|
|
88
|
+
|
|
89
|
+
- **All-day event support** in `create_event` + `calendar_create` MCP tool.
|
|
90
|
+
All-day events emit `VALUE=DATE` per RFC 5545 §3.6.1.
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
- `free_busy` gains `working_hours` and `working_days` parameters; gaps are
|
|
94
|
+
split at day boundaries and trimmed to the working window when set.
|
|
95
|
+
- `courier free` defaults to Mon–Fri 09:00–17:00 local; flags `--all-hours`,
|
|
96
|
+
`--start-hour`, `--end-hour` added.
|
|
97
|
+
- `courier free` CLI shows end date when a slot crosses midnight (previously
|
|
98
|
+
displayed only the end time, which hid the date and looked wrong).
|
|
99
|
+
|
|
100
|
+
### Fixed
|
|
101
|
+
- `free_busy` previously reported multi-day 24/7 gaps (e.g. a 77-hour
|
|
102
|
+
Friday-evening-through-Monday-morning slot) when there were no meetings
|
|
103
|
+
over a weekend. These are now properly constrained and split by day.
|
|
104
|
+
|
|
105
|
+
## [0.1.0] — 2026-04-13
|
|
106
|
+
|
|
107
|
+
- Initial release. JMAP email client, CalDAV calendar client, rule-based
|
|
108
|
+
triage, MCP server, CLI. Fastmail-only.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
Courier is an AI-native email & calendar client — an MCP server (and companion CLI) that talks directly to **JMAP** (email) and **CalDAV** (calendar) as a first-class peer client, not as a wrapper around a human-facing app. It is Fastmail-only today, but `AccountConfig.provider` is intended to be extensible.
|
|
8
|
+
|
|
9
|
+
Two entry points, both defined as `[project.scripts]` in `pyproject.toml`:
|
|
10
|
+
|
|
11
|
+
- `courier` — CLI (`src/courier/cli.py:main`)
|
|
12
|
+
- `courier-mcp` — MCP server over stdio (`src/courier/server.py:main`)
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Install for development (editable + dev extras)
|
|
18
|
+
pip install -e ".[dev]"
|
|
19
|
+
|
|
20
|
+
# Run all tests
|
|
21
|
+
pytest
|
|
22
|
+
|
|
23
|
+
# Run a single test file / test
|
|
24
|
+
pytest tests/test_triage_rules.py
|
|
25
|
+
pytest tests/test_triage_rules.py::test_noreply_sender
|
|
26
|
+
|
|
27
|
+
# Lint
|
|
28
|
+
ruff check .
|
|
29
|
+
|
|
30
|
+
# Run CLI against the configured Fastmail account
|
|
31
|
+
courier setup # first-time account config (prompts for JMAP token + CalDAV app password)
|
|
32
|
+
courier inbox # triaged inbox
|
|
33
|
+
courier new # delta since last check (watermark-based)
|
|
34
|
+
courier status
|
|
35
|
+
|
|
36
|
+
# Run MCP server (stdio)
|
|
37
|
+
courier-mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`pyproject.toml` sets `asyncio_mode = "auto"` for pytest-asyncio, so `async def test_*` functions run without an explicit marker. Ruff is configured for `py311`, line length 100, rules `E,F,I,W`.
|
|
41
|
+
|
|
42
|
+
## Architecture
|
|
43
|
+
|
|
44
|
+
The codebase is a flat package under `src/courier/`. The dependency graph runs one direction: `cli.py` / `server.py` → `triage.py` → `triage_rules.py` → `jmap.py`, with `cal.py`, `state.py`, and `config.py` as leaves. There is no circular coupling.
|
|
45
|
+
|
|
46
|
+
### Layers
|
|
47
|
+
|
|
48
|
+
1. **Protocol clients** (`jmap.py`, `cal.py`)
|
|
49
|
+
- `JMAPClient` implements just enough of RFC 8620/8621 to be a complete email client: session discovery, Email/get, Email/query, Email/set, Mailbox/get, blob download/upload.
|
|
50
|
+
- `Email` (in `jmap.py`) and `Event` (in `cal.py`) are **context-window-optimized** dataclasses — flat, agent-shaped representations with a `summary()`/`summary_dict()` method for token-efficient serialization. They are **not** MIME trees or VObject graphs.
|
|
51
|
+
- `CalDAVClient` bypasses python-caldav's broken Fastmail principal discovery by doing a raw PROPFIND on the calendar-home-set URL. If you touch CalDAV setup, preserve this — it's intentional.
|
|
52
|
+
- `free_busy` respects working hours and splits multi-day gaps at day boundaries; the default for the CLI is Mon–Fri 09:00–17:00 local (see CHANGELOG 0.2.0).
|
|
53
|
+
|
|
54
|
+
2. **Persistence** (`state.py`)
|
|
55
|
+
- `StateStore` is a thin SQLite wrapper. Four tables: `watermarks`, `triage`, `contact_signals`, `session_state`. Every table has `account_id` as part of the primary key — **the schema is multi-account from day one**, even though today only one Fastmail account is configured. Preserve this pattern when adding columns or tables.
|
|
56
|
+
- `contact_signals` tracks per-address send/receive counts and is what `requires_known_contact` / `requires_unknown_contact` triage guards read.
|
|
57
|
+
|
|
58
|
+
3. **Triage** (`triage.py`, `triage_rules.py`, `default_rules.yaml`)
|
|
59
|
+
- `triage.py` is a **backward-compatible facade**: it re-exports `URGENT`/`NEEDS_REPLY`/`ACTIONABLE`/`FYI`/`BULK` constants and wraps `triage_rules.classify_email` / `classify_batch` with a lazy default ruleset. Don't regress this — existing callers (including tests) depend on the old signature.
|
|
60
|
+
- `triage_rules.py` loads YAML rules from `src/courier/default_rules.yaml` (shipped) and merges `~/.config/courier/triage-rules.yaml` (user). Rules are evaluated top-to-bottom; first match wins; no match → `fyi`.
|
|
61
|
+
- User-override merge semantics (important): `sender_overrides` are **additive**, but if the user defines `rules`, those **replace** the defaults entirely. This is documented at the top of `default_rules.yaml`.
|
|
62
|
+
- The default ruleset is lazy-loaded once per process and **never invalidated** — rule file changes require a restart.
|
|
63
|
+
|
|
64
|
+
4. **Config** (`config.py`)
|
|
65
|
+
- JSON config at `~/.config/courier/config.json`. `AccountConfig` holds **two** credentials per account: a JMAP API token (`api_token`) for email and a CalDAV app password (`app_password`) for calendar — Fastmail issues these separately.
|
|
66
|
+
- `db_path` defaults to `~/.config/courier/courier.db`.
|
|
67
|
+
|
|
68
|
+
5. **Surfaces** (`cli.py`, `server.py`)
|
|
69
|
+
- `server.py` defines the MCP tool surface (see README's tool table) and uses module-level singletons (`_config`, `_jmap`, `_caldav`, `_state`) lazily initialized on first use. The MCP tool names are the public API — renaming them is a breaking change for anyone who has added Courier to their Claude/agent config.
|
|
70
|
+
- `server._ensure_list()` exists because some MCP runtimes (e.g. Cowork) serialize arrays as JSON strings. Keep the coercion when adding new batch tools.
|
|
71
|
+
- The MCP layer passes the Fastmail account's local timezone (`account.timezone`, default `America/Chicago`) into `Event.summary_dict(local_tz=...)` so calendar output is rendered in local time.
|
|
72
|
+
|
|
73
|
+
### A few load-bearing conventions
|
|
74
|
+
|
|
75
|
+
- `Email.summary(max_body=...)` is the token-budget knob. `email_read` uses 10 000 chars; `email_thread` uses 2 000 per message; list endpoints use the 500 default. Respect this when adding new read endpoints.
|
|
76
|
+
- `email_new` is watermark-driven: it reads `watermarks` for the inbox, filters, then writes the newest seen `date`+`id` back. Don't short-circuit this — the whole "what's new since last session" UX depends on it.
|
|
77
|
+
- All-day events use `VALUE=DATE` (RFC 5545 §3.6.1). `server.calendar_create` sniffs `"T" not in raw_start` to decide all-day vs. timed; `cal.create_event(all_day=True)` is the underlying flag. Don't route bare-date strings through `datetime.fromisoformat`.
|
|
78
|
+
- `Email.has_calendar_invite` is populated by walking JMAP `bodyStructure` for `text/calendar` parts and is consumed by the `requires_calendar_invite` triage guard and the `calendar-invite-mime` default rule.
|
|
79
|
+
|
|
80
|
+
## Testing notes
|
|
81
|
+
|
|
82
|
+
- Tests are pure unit tests against the dataclasses and state layer — nothing hits the network. `tests/conftest.py` provides an `account` fixture and a `make_email(**overrides)` factory; prefer these over constructing `Email` objects inline.
|
|
83
|
+
- `test_triage_rules.py` is the largest suite and exercises the YAML loader, guards, and override semantics. When changing `default_rules.yaml` or adding rule guards, extend that file.
|
|
84
|
+
- There is no end-to-end JMAP/CalDAV test — connection behaviour is exercised manually via `courier status` / `courier setup`.
|
|
85
|
+
- `tests/e2e/` is the opt-in live integration suite, gated by `COURIER_LIVE_TESTS=1`. It uses a sandbox mailbox `Courier/Tests` and calendar `Courier Tests`, both created by `courier test-setup`. Every write test self-cleans via a `self_sent_email` fixture that sends a timestamped test message to the configured account, yields it, and trashes both copies on teardown. Do not add live tests outside `tests/e2e/`.
|
|
86
|
+
- `asyncio_default_test_loop_scope = "session"` is set in `pyproject.toml` so function-scoped tests share the same event loop as session-scoped fixtures (required for reusing the `live_jmap` httpx client). Don't change this without checking every async fixture.
|
|
87
|
+
|
|
88
|
+
## Scope discipline
|
|
89
|
+
|
|
90
|
+
- Don't introduce a provider abstraction for a second provider that doesn't exist yet. `AccountConfig.provider` is a string and `cal.py` already branches on `"fastmail"` for URL auto-build — that's the pattern to extend.
|
|
91
|
+
- Don't add fallback strategies (HTML-to-text scraping, IMAP fallback, etc.) on your own initiative. Fail fast and loud; the surrounding user instructions in `~/.claude/CLAUDE.md` explicitly prohibit silent fallbacks.
|
ai_courier-0.3.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jim Perry / Harness Intelligence
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ai-courier
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: AI-native email & calendar client over JMAP and CalDAV
|
|
5
|
+
Author-email: Jim Perry <jim@hi-team.net>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: agent,ai,caldav,calendar,email,jmap,mcp
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Communications :: Email
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Requires-Dist: caldav>=1.3
|
|
17
|
+
Requires-Dist: httpx>=0.27
|
|
18
|
+
Requires-Dist: mcp>=1.0
|
|
19
|
+
Requires-Dist: pyyaml>=6.0
|
|
20
|
+
Requires-Dist: vobject>=0.9
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# Courier
|
|
28
|
+
|
|
29
|
+
**AI-native email & calendar client over JMAP and CalDAV.**
|
|
30
|
+
|
|
31
|
+
Your AI agent deserves its own email client — not an adapter wrapping a human one.
|
|
32
|
+
|
|
33
|
+
## The Problem
|
|
34
|
+
|
|
35
|
+
Every AI agent framework connects to email and calendar by wrapping human-facing apps. MCP servers adapt web UIs. Osascript automates native mail clients. Browser tools click through webmail. Each introduces friction because the agent is fighting an interface designed for someone else.
|
|
36
|
+
|
|
37
|
+
Courier takes a different approach: connect directly to the open protocols (JMAP for email, CalDAV for calendar) as a first-class client — a peer alongside your human apps, not a wrapper around them.
|
|
38
|
+
|
|
39
|
+
## What Makes It Different
|
|
40
|
+
|
|
41
|
+
- **Context-window-optimized output** — emails are structured for AI consumption, not HTML rendering
|
|
42
|
+
- **Session state & watermarks** — "what's new?" is a first-class operation that persists between sessions
|
|
43
|
+
- **Triage classification** — emails arrive pre-sorted: urgent, needs_reply, actionable, fyi, bulk
|
|
44
|
+
- **Batch-first operations** — archive 15 emails in one call, not 15 separate requests
|
|
45
|
+
- **Sane timezone handling** — CalDAV with proper TZID normalization and recurrence expansion
|
|
46
|
+
- **Composable** — higher-order operations like "find all emails from this person and summarize the thread"
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install ai-courier
|
|
52
|
+
|
|
53
|
+
# Configure your Fastmail account
|
|
54
|
+
courier setup
|
|
55
|
+
|
|
56
|
+
# Check your inbox (triaged by priority)
|
|
57
|
+
courier inbox
|
|
58
|
+
|
|
59
|
+
# What's new since last check?
|
|
60
|
+
courier new
|
|
61
|
+
|
|
62
|
+
# Today's calendar
|
|
63
|
+
courier today
|
|
64
|
+
|
|
65
|
+
# Find free time
|
|
66
|
+
courier free
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## As an MCP Server
|
|
70
|
+
|
|
71
|
+
Add to your Claude configuration:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"mcpServers": {
|
|
76
|
+
"courier": {
|
|
77
|
+
"command": "courier-mcp"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Available tools:
|
|
84
|
+
|
|
85
|
+
| Tool | Description |
|
|
86
|
+
|------|-------------|
|
|
87
|
+
| `email_inbox` | Triaged inbox (urgent → bulk) |
|
|
88
|
+
| `email_new` | New emails since last check (watermark-based) |
|
|
89
|
+
| `email_search` | Flexible email search |
|
|
90
|
+
| `email_read` | Read full email by ID |
|
|
91
|
+
| `email_thread` | Get full thread |
|
|
92
|
+
| `email_archive` | Archive emails (batch) |
|
|
93
|
+
| `email_trash` | Trash emails (batch) |
|
|
94
|
+
| `email_mark_read` | Mark read (batch) |
|
|
95
|
+
| `email_draft` | Create draft |
|
|
96
|
+
| `email_send` | Send email |
|
|
97
|
+
| `email_mailboxes` | List folders/labels with IDs, paths, roles |
|
|
98
|
+
| `email_move` | Move emails to a folder (by ID, name, or path) |
|
|
99
|
+
| `email_label` | Add/remove labels without touching other memberships |
|
|
100
|
+
| `email_reply` | Threaded reply (In-Reply-To + References); draft or send |
|
|
101
|
+
| `calendar_today` | Today's events |
|
|
102
|
+
| `calendar_week` | This week's events |
|
|
103
|
+
| `calendar_events` | Events in date range |
|
|
104
|
+
| `calendar_free` | Find free time slots |
|
|
105
|
+
| `calendar_create` | Create event |
|
|
106
|
+
| `courier_status` | Connection & watermark status |
|
|
107
|
+
|
|
108
|
+
## Customizing Triage Rules
|
|
109
|
+
|
|
110
|
+
Triage rules are defined in YAML. The defaults ship with Courier (see `src/courier/default_rules.yaml`). To customize, create `~/.config/courier/triage-rules.yaml`:
|
|
111
|
+
|
|
112
|
+
```yaml
|
|
113
|
+
# Force specific senders to a classification (checked first, confidence 1.0)
|
|
114
|
+
sender_overrides:
|
|
115
|
+
"ceo@mycompany.com": { classification: urgent, reason: "VIP sender" }
|
|
116
|
+
"deals@spammy.com": { classification: bulk, reason: "always bulk" }
|
|
117
|
+
|
|
118
|
+
# Custom rules — if you define any, they REPLACE the defaults entirely.
|
|
119
|
+
# Omit this section to keep defaults and only add sender overrides.
|
|
120
|
+
rules:
|
|
121
|
+
- name: my-custom-rule
|
|
122
|
+
classification: urgent
|
|
123
|
+
confidence: 0.9
|
|
124
|
+
reason: "keyword: '{match}'"
|
|
125
|
+
subject_pattern: "\\b(fire|outage|p0)\\b"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Each rule supports regex match conditions (`from_pattern`, `subject_pattern`, `body_pattern`, `domain_pattern`) and guard conditions (`requires_known_contact`, `requires_unknown_contact`, `max_size`, `requires_thread`, `requires_flagged`). Rules are evaluated top-to-bottom; first match wins.
|
|
129
|
+
|
|
130
|
+
See `src/courier/default_rules.yaml` for the full schema documentation and all default rules.
|
|
131
|
+
|
|
132
|
+
## Architecture
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
AI Agent (Claude, GPT, etc.)
|
|
136
|
+
│
|
|
137
|
+
▼
|
|
138
|
+
Courier MCP Server ← the novel layer
|
|
139
|
+
├── Email (JMAP) ├── Calendar (CalDAV)
|
|
140
|
+
│ ├── Watermarks │ ├── TZID normalization
|
|
141
|
+
│ ├── Triage │ ├── Recurrence expansion
|
|
142
|
+
│ └── Batch ops │ └── Free/busy
|
|
143
|
+
└────────────────────┘
|
|
144
|
+
│
|
|
145
|
+
▼
|
|
146
|
+
SQLite (state, watermarks, contact signals)
|
|
147
|
+
│
|
|
148
|
+
▼
|
|
149
|
+
JMAP API ──── CalDAV API
|
|
150
|
+
(Fastmail) (Fastmail)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Courier talks to the same backend your human apps do. It's a parallel client, not a wrapper.
|
|
154
|
+
|
|
155
|
+
## Requirements
|
|
156
|
+
|
|
157
|
+
- Python 3.11+
|
|
158
|
+
- A Fastmail account with an API token ([get one here](https://www.fastmail.com/settings/security/tokens))
|
|
159
|
+
- Scopes needed: Mail, Calendars (Contacts optional)
|
|
160
|
+
|
|
161
|
+
## Development
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
git clone https://github.com/iamdadzilla/courier.git
|
|
165
|
+
cd courier
|
|
166
|
+
pip install -e ".[dev]"
|
|
167
|
+
pytest
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Why "Courier"?
|
|
171
|
+
|
|
172
|
+
A courier delivers messages directly. No intermediary, no adapter, no wrapper. Just the message.
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Courier
|
|
2
|
+
|
|
3
|
+
**AI-native email & calendar client over JMAP and CalDAV.**
|
|
4
|
+
|
|
5
|
+
Your AI agent deserves its own email client — not an adapter wrapping a human one.
|
|
6
|
+
|
|
7
|
+
## The Problem
|
|
8
|
+
|
|
9
|
+
Every AI agent framework connects to email and calendar by wrapping human-facing apps. MCP servers adapt web UIs. Osascript automates native mail clients. Browser tools click through webmail. Each introduces friction because the agent is fighting an interface designed for someone else.
|
|
10
|
+
|
|
11
|
+
Courier takes a different approach: connect directly to the open protocols (JMAP for email, CalDAV for calendar) as a first-class client — a peer alongside your human apps, not a wrapper around them.
|
|
12
|
+
|
|
13
|
+
## What Makes It Different
|
|
14
|
+
|
|
15
|
+
- **Context-window-optimized output** — emails are structured for AI consumption, not HTML rendering
|
|
16
|
+
- **Session state & watermarks** — "what's new?" is a first-class operation that persists between sessions
|
|
17
|
+
- **Triage classification** — emails arrive pre-sorted: urgent, needs_reply, actionable, fyi, bulk
|
|
18
|
+
- **Batch-first operations** — archive 15 emails in one call, not 15 separate requests
|
|
19
|
+
- **Sane timezone handling** — CalDAV with proper TZID normalization and recurrence expansion
|
|
20
|
+
- **Composable** — higher-order operations like "find all emails from this person and summarize the thread"
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install ai-courier
|
|
26
|
+
|
|
27
|
+
# Configure your Fastmail account
|
|
28
|
+
courier setup
|
|
29
|
+
|
|
30
|
+
# Check your inbox (triaged by priority)
|
|
31
|
+
courier inbox
|
|
32
|
+
|
|
33
|
+
# What's new since last check?
|
|
34
|
+
courier new
|
|
35
|
+
|
|
36
|
+
# Today's calendar
|
|
37
|
+
courier today
|
|
38
|
+
|
|
39
|
+
# Find free time
|
|
40
|
+
courier free
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## As an MCP Server
|
|
44
|
+
|
|
45
|
+
Add to your Claude configuration:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"courier": {
|
|
51
|
+
"command": "courier-mcp"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Available tools:
|
|
58
|
+
|
|
59
|
+
| Tool | Description |
|
|
60
|
+
|------|-------------|
|
|
61
|
+
| `email_inbox` | Triaged inbox (urgent → bulk) |
|
|
62
|
+
| `email_new` | New emails since last check (watermark-based) |
|
|
63
|
+
| `email_search` | Flexible email search |
|
|
64
|
+
| `email_read` | Read full email by ID |
|
|
65
|
+
| `email_thread` | Get full thread |
|
|
66
|
+
| `email_archive` | Archive emails (batch) |
|
|
67
|
+
| `email_trash` | Trash emails (batch) |
|
|
68
|
+
| `email_mark_read` | Mark read (batch) |
|
|
69
|
+
| `email_draft` | Create draft |
|
|
70
|
+
| `email_send` | Send email |
|
|
71
|
+
| `email_mailboxes` | List folders/labels with IDs, paths, roles |
|
|
72
|
+
| `email_move` | Move emails to a folder (by ID, name, or path) |
|
|
73
|
+
| `email_label` | Add/remove labels without touching other memberships |
|
|
74
|
+
| `email_reply` | Threaded reply (In-Reply-To + References); draft or send |
|
|
75
|
+
| `calendar_today` | Today's events |
|
|
76
|
+
| `calendar_week` | This week's events |
|
|
77
|
+
| `calendar_events` | Events in date range |
|
|
78
|
+
| `calendar_free` | Find free time slots |
|
|
79
|
+
| `calendar_create` | Create event |
|
|
80
|
+
| `courier_status` | Connection & watermark status |
|
|
81
|
+
|
|
82
|
+
## Customizing Triage Rules
|
|
83
|
+
|
|
84
|
+
Triage rules are defined in YAML. The defaults ship with Courier (see `src/courier/default_rules.yaml`). To customize, create `~/.config/courier/triage-rules.yaml`:
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
# Force specific senders to a classification (checked first, confidence 1.0)
|
|
88
|
+
sender_overrides:
|
|
89
|
+
"ceo@mycompany.com": { classification: urgent, reason: "VIP sender" }
|
|
90
|
+
"deals@spammy.com": { classification: bulk, reason: "always bulk" }
|
|
91
|
+
|
|
92
|
+
# Custom rules — if you define any, they REPLACE the defaults entirely.
|
|
93
|
+
# Omit this section to keep defaults and only add sender overrides.
|
|
94
|
+
rules:
|
|
95
|
+
- name: my-custom-rule
|
|
96
|
+
classification: urgent
|
|
97
|
+
confidence: 0.9
|
|
98
|
+
reason: "keyword: '{match}'"
|
|
99
|
+
subject_pattern: "\\b(fire|outage|p0)\\b"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Each rule supports regex match conditions (`from_pattern`, `subject_pattern`, `body_pattern`, `domain_pattern`) and guard conditions (`requires_known_contact`, `requires_unknown_contact`, `max_size`, `requires_thread`, `requires_flagged`). Rules are evaluated top-to-bottom; first match wins.
|
|
103
|
+
|
|
104
|
+
See `src/courier/default_rules.yaml` for the full schema documentation and all default rules.
|
|
105
|
+
|
|
106
|
+
## Architecture
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
AI Agent (Claude, GPT, etc.)
|
|
110
|
+
│
|
|
111
|
+
▼
|
|
112
|
+
Courier MCP Server ← the novel layer
|
|
113
|
+
├── Email (JMAP) ├── Calendar (CalDAV)
|
|
114
|
+
│ ├── Watermarks │ ├── TZID normalization
|
|
115
|
+
│ ├── Triage │ ├── Recurrence expansion
|
|
116
|
+
│ └── Batch ops │ └── Free/busy
|
|
117
|
+
└────────────────────┘
|
|
118
|
+
│
|
|
119
|
+
▼
|
|
120
|
+
SQLite (state, watermarks, contact signals)
|
|
121
|
+
│
|
|
122
|
+
▼
|
|
123
|
+
JMAP API ──── CalDAV API
|
|
124
|
+
(Fastmail) (Fastmail)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Courier talks to the same backend your human apps do. It's a parallel client, not a wrapper.
|
|
128
|
+
|
|
129
|
+
## Requirements
|
|
130
|
+
|
|
131
|
+
- Python 3.11+
|
|
132
|
+
- A Fastmail account with an API token ([get one here](https://www.fastmail.com/settings/security/tokens))
|
|
133
|
+
- Scopes needed: Mail, Calendars (Contacts optional)
|
|
134
|
+
|
|
135
|
+
## Development
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
git clone https://github.com/iamdadzilla/courier.git
|
|
139
|
+
cd courier
|
|
140
|
+
pip install -e ".[dev]"
|
|
141
|
+
pytest
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Why "Courier"?
|
|
145
|
+
|
|
146
|
+
A courier delivers messages directly. No intermediary, no adapter, no wrapper. Just the message.
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
## Summary
|
|
2
|
+
|
|
3
|
+
Ships Courier v0.3: **retires Fastmail MCP from the sweep entirely.**
|
|
4
|
+
|
|
5
|
+
- **Four new MCP tools** — `email_mailboxes` (discover folders/labels with paths), `email_move` (file by ID/name/path), `email_label` (non-destructive multi-tagging), `email_reply` (threaded In-Reply-To + References + `Re:` prefix, draft or send)
|
|
6
|
+
- **Opt-in E2E suite** — `tests/e2e/` gated by `COURIER_LIVE_TESTS=1`, 19 live tests against real Fastmail, self-cleaning via `Courier/Tests` sandbox (created by new `courier test-setup` command)
|
|
7
|
+
- **Three v0.2 bugs caught and fixed along the way** — asyncio test-loop scope mismatch, `_normalize_utcdate` silent acceptance of non-UTC offsets, `create_draft` sending `inReplyTo` as a string instead of `String[]` per RFC 8621 §4.1.3.1
|
|
8
|
+
|
|
9
|
+
All three "stale?" vault notes definitively confirmed stale: attachment send, attachment download, and all-day events (`VALUE=DATE` per RFC 5545 §3.6.1) all work end-to-end. Hi-Team skill library and system docs scrubbed of Fastmail MCP references.
|
|
10
|
+
|
|
11
|
+
23 commits, +1480/-30 across 20 files. Spec: [`docs/superpowers/specs/2026-04-17-courier-v0.3-design.md`](docs/superpowers/specs/2026-04-17-courier-v0.3-design.md). Plan: [`docs/superpowers/plans/2026-04-17-courier-v0.3.md`](docs/superpowers/plans/2026-04-17-courier-v0.3.md).
|
|
12
|
+
|
|
13
|
+
## Test plan
|
|
14
|
+
|
|
15
|
+
- [ ] `pytest tests/ --ignore=tests/e2e -v` — 241 passed
|
|
16
|
+
- [ ] `COURIER_LIVE_TESTS=1 pytest tests/e2e/ -v` — 19 passed (rerun once on fixture-poll-ceiling flake if it hits)
|
|
17
|
+
- [ ] `ruff check .` — 51 errors (flat baseline; zero new introduced by v0.3)
|
|
18
|
+
- [ ] `courier --help` shows new subcommands: `test-setup`, `mailboxes`, `move`, `label`, `reply`
|
|
19
|
+
- [ ] `python -c "import courier; print(courier.__version__)"` → `0.3.0`
|
|
20
|
+
- [ ] **Acceptance:** disconnect Fastmail MCP in `~/.claude/settings.json`, run one full morning sweep — completes cleanly with Courier only
|
|
21
|
+
- [ ] Richard onboards per `Knowledge/System/richard-setup-guide.md` Part 5 with his own Fastmail creds and runs a live sweep
|
|
22
|
+
|
|
23
|
+
## Post-merge
|
|
24
|
+
|
|
25
|
+
- Tag `v0.3.0` and publish to PyPI
|
|
26
|
+
- Vault skill library already updated on the `main` vault (Sync.com-synced, not git-tracked)
|
|
27
|
+
- Update `Knowledge/Tech/Courier/courier-sweep-log.md` after the first Fastmail-MCP-free sweep confirms acceptance
|