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.
Files changed (40) hide show
  1. ai_courier-0.3.2/.claude/settings.local.json +20 -0
  2. ai_courier-0.3.2/.gitignore +15 -0
  3. ai_courier-0.3.2/CHANGELOG.md +108 -0
  4. ai_courier-0.3.2/CLAUDE.md +91 -0
  5. ai_courier-0.3.2/LICENSE +21 -0
  6. ai_courier-0.3.2/PKG-INFO +176 -0
  7. ai_courier-0.3.2/README.md +150 -0
  8. ai_courier-0.3.2/docs/superpowers/plans/2026-04-17-courier-v0.3-pr-body.md +27 -0
  9. ai_courier-0.3.2/docs/superpowers/plans/2026-04-17-courier-v0.3-release-notes.md +107 -0
  10. ai_courier-0.3.2/docs/superpowers/plans/2026-04-17-courier-v0.3.md +2305 -0
  11. ai_courier-0.3.2/docs/superpowers/specs/2026-04-17-courier-v0.3-design.md +339 -0
  12. ai_courier-0.3.2/pyproject.toml +62 -0
  13. ai_courier-0.3.2/src/courier/__init__.py +3 -0
  14. ai_courier-0.3.2/src/courier/cal.py +504 -0
  15. ai_courier-0.3.2/src/courier/cli.py +628 -0
  16. ai_courier-0.3.2/src/courier/config.py +80 -0
  17. ai_courier-0.3.2/src/courier/default_rules.yaml +139 -0
  18. ai_courier-0.3.2/src/courier/jmap.py +1041 -0
  19. ai_courier-0.3.2/src/courier/server.py +649 -0
  20. ai_courier-0.3.2/src/courier/state.py +220 -0
  21. ai_courier-0.3.2/src/courier/triage.py +76 -0
  22. ai_courier-0.3.2/src/courier/triage_rules.py +401 -0
  23. ai_courier-0.3.2/tests/__init__.py +0 -0
  24. ai_courier-0.3.2/tests/conftest.py +48 -0
  25. ai_courier-0.3.2/tests/e2e/__init__.py +0 -0
  26. ai_courier-0.3.2/tests/e2e/conftest.py +153 -0
  27. ai_courier-0.3.2/tests/e2e/test_calendar_v02.py +106 -0
  28. ai_courier-0.3.2/tests/e2e/test_email_attachments_v02.py +102 -0
  29. ai_courier-0.3.2/tests/e2e/test_email_read_v02.py +46 -0
  30. ai_courier-0.3.2/tests/e2e/test_email_v03.py +142 -0
  31. ai_courier-0.3.2/tests/e2e/test_email_write_v02.py +62 -0
  32. ai_courier-0.3.2/tests/test_cal.py +391 -0
  33. ai_courier-0.3.2/tests/test_config.py +117 -0
  34. ai_courier-0.3.2/tests/test_jmap.py +395 -0
  35. ai_courier-0.3.2/tests/test_jmap_v03.py +258 -0
  36. ai_courier-0.3.2/tests/test_server.py +48 -0
  37. ai_courier-0.3.2/tests/test_state.py +148 -0
  38. ai_courier-0.3.2/tests/test_triage.py +279 -0
  39. ai_courier-0.3.2/tests/test_triage_rules.py +670 -0
  40. 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,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .eggs/
7
+ *.egg
8
+ .venv/
9
+ venv/
10
+ .env
11
+ *.db
12
+ config.json
13
+ .ruff_cache/
14
+ .pytest_cache/
15
+ .mypy_cache/
@@ -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.
@@ -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