Courier
Fastmail-only Alpha preview. An AI-native email & calendar client over JMAP and CalDAV.
Your AI agent deserves its own email client — not an adapter wrapping a human one.
Not Courier Inc. / trycourier.com (notification API). This project's PyPI name is
ai-courier. The CLI iscourier; the MCP server command iscourier-mcp.
Today Courier talks to Fastmail only. Gmail, iCloud, and Exchange are not supported — they are contribution targets, not a cold-install path. courier setup configures a Fastmail JMAP token plus a Fastmail CalDAV app password.
MCP write tools are gated by trust_level in ~/.config/courier/config.json (read → file → correspond → manage). courier setup defaults new installs to file (read + archive/move/label/drafts). Existing configs that omit the field stay at manage so upgrades do not revoke send/trash. An agent cannot raise its own trust level.
The Problem
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.
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.
What Makes It Different
- Context-window-optimized output — emails are structured for AI consumption, not HTML rendering
- Session state & watermarks — "what's new?" is a first-class operation that persists between sessions
- Triage classification — emails arrive pre-sorted: urgent, needs_reply, actionable, fyi, bulk
- Batch-first operations — archive 15 emails in one call, not 15 separate requests
- Real scheduling, not just storage — RSVPs send iTIP REPLY to the organizer; events with attendees send real invitations
- Identity-aware sending —
fromon every write tool picks the matching Fastmail identity (alias) per call - Contacts as a first-class surface — full CRUD: lookup, search, create, update, delete (JMAP Contacts, RFC 9610/9553)
- Sane timezone handling — TZID normalization, recurrence expansion, and input datetimes that land at the right instant (offsets honored; naive = account-local).
courier setupasks for the IANA timezone. - Composable — higher-order operations like "find all emails from this person and summarize the thread"
Quick Start
pip install ai-courier # or: uv tool install ai-courier
# Configure your Fastmail account
courier setup
# Check your inbox (triaged by priority)
courier inbox
# What's new since last check?
courier new
# Today's calendar
courier today
# Find free time
courier free
mcp 2.x: Courier 0.8.5+ pins mcp>=1.28,<2. Releases before 0.8.5
declared mcp>=1.0, so an unpinned uvx / pip install can pull mcp 2.x
and crash courier-mcp on import (Server has no list_tools). Upgrade
to 0.8.5+ (0.9.0 recommended), or pin the extra while you wait:
uvx --from ai-courier --with 'mcp>=1.12,<2' courier-mcp
As an MCP Server
Add to your Claude configuration:
{
"mcpServers": {
"courier": {
"command": "courier-mcp"
}
}
}
list_tools only returns tools allowed at the configured trust_level. A
file-tier agent never sees email_send. Calling a hidden tool still fails
loudly with the required tier. See docs/THREAT_MODEL.md.
Available tools (full manage surface):
Email — read & triage
| Tool | Description |
|---|---|
email_inbox |
Triaged inbox (urgent → bulk), 200-char snippets, saturation signal |
email_new |
New emails since last check (watermark-based) |
email_search |
Flexible search, including by mailbox (folder ID, name, or path) |
email_sent_since |
Recent sent messages (queries Sent mailbox directly) |
email_read |
Read full email by ID |
email_thread |
Get full thread |
email_thread_digest |
Quote-stripped, token-budgeted thread payload |
email_drafts |
Authoritative Drafts listing with created_at / last_modified_at |
email_attachments |
List attachments on an email |
email_attachment_save |
Download an attachment to disk |
email_mailboxes |
List folders/labels with IDs, paths, roles |
Email — write & file
| Tool | Description |
|---|---|
email_send |
Send email (plain or HTML; optional from identity; optional attachment_paths) |
email_reply |
Threaded reply (In-Reply-To + References); draft or send; optional attachment_paths; replies to alias-addressed mail default to that alias |
email_forward |
Forward an email, re-attaches original as .eml; draft or send |
email_draft |
Create draft (optional attachment_paths) |
email_archive |
Archive emails (batch; optional atomic label add/remove) |
email_trash |
Trash emails (batch) |
email_mark_read |
Mark read (batch) |
email_move |
Move emails to a folder (by ID, name, or path) |
email_label |
Add/remove labels without touching other memberships |
list_identities |
Sending identities (aliases) available to from |
Contacts
| Tool | Description |
|---|---|
contact_lookup |
Full contact record by email address |
contact_search |
Search contacts by name / text |
contact_create |
Create a contact (name, emails, phones, org, title, notes) |
contact_update |
Update a contact — name/org/title/notes replace; emails/phones append |
contact_delete |
Delete a contact — permanent; there is no trash can for contacts |
Calendar
| Tool | Description |
|---|---|
calendar_today |
Today's events |
calendar_week |
This week's events |
calendar_events |
Events in date range |
calendar_search |
Text search across summary / location / description |
calendar_free |
Find free time slots (respects working hours) |
calendar_free_both |
Free slots intersected across primary + delegate accounts |
calendar_create |
Create event — attendees get real iTIP REQUEST invitations (notify_attendees=false to skip) |
calendar_update |
Update event fields |
calendar_move |
Move event between calendars |
calendar_delete |
Delete event |
calendar_rsvp |
Respond to an invite — sends iTIP REPLY to the organizer (notify_organizer=false to skip) |
Status
| Tool | Description |
|---|---|
courier_status |
Live connection probe, watermark status, server version |
Customizing Triage Rules
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:
# Force specific senders to a classification (checked first, confidence 1.0)
sender_overrides:
"ceo@mycompany.com": { classification: urgent, reason: "VIP sender" }
"deals@spammy.com": { classification: bulk, reason: "always bulk" }
# Custom rules — if you define any, they REPLACE the defaults entirely.
# Omit this section to keep defaults and only add sender overrides.
rules:
- name: my-custom-rule
classification: urgent
confidence: 0.9
reason: "keyword: '{match}'"
subject_pattern: "\\b(fire|outage|p0)\\b"
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.
See src/courier/default_rules.yaml for the full schema documentation and all default rules.
As a Python Library
For in-process consumers (cron scripts, pipelines) that don't want to host an MCP server or shell out to the CLI:
from courier import Client
client = Client() # lazy connect, reads ~/.config/courier/config.json
for email in client.email_new(): # shares the MCP server's watermark
if looks_routine(email):
client.email_archive([email.id])
The library surface is deliberately narrow — read, label, archive, mark-read. No send, no calendar. Write methods honor trust_level (a read config cannot archive).
Architecture
AI Agent (Claude, GPT, etc.)
│
▼
Courier MCP Server ← the novel layer
├── Email (JMAP) ├── Calendar (CalDAV) ├── Contacts (JMAP)
│ ├── Watermarks │ ├── TZID handling │ ├── Lookup/search
│ ├── Triage │ ├── Recurrence │ └── Create/update
│ ├── Identities │ ├── Free/busy
│ └── Batch ops │ └── iTIP REPLY/REQUEST
└─────────────────────┘
│
▼
SQLite (state, watermarks, contact signals)
│
▼
JMAP API ──── CalDAV API
(Fastmail) (Fastmail)
Courier talks to the same backend your human apps do. It's a parallel client, not a wrapper.
Requirements
- Python 3.11+
- A Fastmail account with two credentials (Fastmail issues these separately;
courier setupprompts for both):- a JMAP API token (get one here) — scopes: Mail, and Contacts if you want the contact tools
- a CalDAV app password — for the calendar tools
Other JMAP/CalDAV providers are on the roadmap (Gmail #2, iCloud #3, Exchange #1). Do not invent a second provider on speculation — see CONTRIBUTING.md.
Development
git clone https://github.com/iamdadzilla/courier.git
cd courier
pip install -e ".[dev]"
pytest
See CONTRIBUTING.md for tests and scope, SECURITY.md to report issues, CHANGELOG.md for release history, and docs/ROADMAP.md for the generated feature roadmap.
Why "Courier"?
A courier delivers messages directly. No intermediary, no adapter, no wrapper. Just the message.
License
MIT
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 ai_courier-0.9.1.tar.gz.
File metadata
- Download URL: ai_courier-0.9.1.tar.gz
- Upload date:
- Size: 290.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e1c7cae7d2e51a2f08c723f2e5a987d85cef86766e3d49c15b1a1c0af027446a
|
|
| MD5 |
cbc3876cf204010893eedf41ea4fae2e
|
|
| BLAKE2b-256 |
83f2fe498cb5d8f8d324d484fd871d2a8e5f8946c130ebc1bfa5837120f678bf
|
Provenance
The following attestation bundles were made for ai_courier-0.9.1.tar.gz:
Publisher:
publish.yml on iamdadzilla/courier
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ai_courier-0.9.1.tar.gz -
Subject digest:
e1c7cae7d2e51a2f08c723f2e5a987d85cef86766e3d49c15b1a1c0af027446a - Sigstore transparency entry: 2795669427
- Sigstore integration time:
-
Permalink:
iamdadzilla/courier@853175c8d2952c9d5447d977aa70fa33cfae41ca -
Branch / Tag:
refs/tags/v0.9.1 - Owner: https://github.com/iamdadzilla
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@853175c8d2952c9d5447d977aa70fa33cfae41ca -
Trigger Event:
push
-
Statement type:
File details
Details for the file ai_courier-0.9.1-py3-none-any.whl.
File metadata
- Download URL: ai_courier-0.9.1-py3-none-any.whl
- Upload date:
- Size: 96.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ab4a217b3618c6dde76bcc3410b146faf74c4183afa701e30f98cee0b0a6de73
|
|
| MD5 |
352a32c3a7574a9eaa0988998bedc0b2
|
|
| BLAKE2b-256 |
6300264b0dfee6c48f5c63fb7891230ee7cb76f460a56faf325a12605829e4c4
|
Provenance
The following attestation bundles were made for ai_courier-0.9.1-py3-none-any.whl:
Publisher:
publish.yml on iamdadzilla/courier
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ai_courier-0.9.1-py3-none-any.whl -
Subject digest:
ab4a217b3618c6dde76bcc3410b146faf74c4183afa701e30f98cee0b0a6de73 - Sigstore transparency entry: 2795669502
- Sigstore integration time:
-
Permalink:
iamdadzilla/courier@853175c8d2952c9d5447d977aa70fa33cfae41ca -
Branch / Tag:
refs/tags/v0.9.1 - Owner: https://github.com/iamdadzilla
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@853175c8d2952c9d5447d977aa70fa33cfae41ca -
Trigger Event:
push
-
Statement type: