Python SDK for Fastmail's CalDAV API — calendars, events, and iCalendar round-tripping
Project description
fastmail-py-sdk is an async Python SDK for the Fastmail platform, providing first-class clients for email (JMAP), calendars (CalDAV), and contacts (CardDAV). It's a faithful port of the Rust fastmail-cli tool, with Pydantic-typed domain models, RFC 5545-compliant iCalendar round-tripping, and ETag-based optimistic concurrency throughout.
✨ Features
- 📧 JMAP email — list, search, send, reply, forward, move, mark read/spam, download attachments, and manage masked email addresses
- 📅 CalDAV calendars — discover, create, update, and delete calendars; full CRUD for events with attendees, recurrences, and reminders
- 👤 CardDAV contacts — list, search, create, update, and delete vCard 4.0 contacts; manage contact groups with member add/remove
- 🔄 iCalendar parsing — hand-rolled RFC 5545
parser and serializer that handles Fastmail's iCalendar dialect, with
icalendarlibrary fallback - 🛡️ Optimistic concurrency — ETag-based conflict detection on every write, matching the Rust CLI's durability guarantees
- ⚡ Partial-failure tolerance — concurrent per-calendar/address-book fetches using asyncio.gather that log and skip individual failures instead of aborting
- 🔑 Credential compatibility — reads credentials from env vars or the
Rust
fastmail-cliconfig file (~/.config/fastmail-cli/config.toml) - 🧪 Tested — respx-powered HTTP mocking ensures the full test suite runs without a Fastmail account
- 🐍 Fully typed — Pydantic v2 models with
camelCasealiases for seamless JMAP API round-tripping
🚀 Quick Start
# Install
uv add fastmail-py-sdk
# Or with pip
pip install fastmail-py-sdk
import asyncio
from fastmail_sdk import JmapClient, CalDavClient, load_credentials
# --- JMAP (email) ---
async def check_inbox():
async with JmapClient(token="YOUR_API_TOKEN") as client:
await client.authenticate()
inbox = await client.find_mailbox("inbox")
emails = await client.list_emails(inbox.id, limit=5)
for email in emails:
print(email.subject, "-", email.from_[0].email if email.from_ else "unknown")
# --- CalDAV (calendars) ---
async def list_events():
username, app_password = load_credentials()
async with CalDavClient(username, app_password) as client:
calendars = await client.list_calendars()
for cal in calendars:
print(f"📅 {cal.name} ({cal.id})")
asyncio.run(check_inbox())
📖 Table of Contents
📦 Installation
With uv (recommended)
uv is a fast Python package manager.
uv add fastmail-py-sdk
With pip
pip install fastmail-py-sdk
From source
git clone https://github.com/kerryhatcher/fastmail-py-sdk.git
cd fastmail-py-sdk
uv sync
Requires Python 3.13+.
💡 Why fastmail-py-sdk?
Fastmail offers three API surfaces — JMAP for email, CalDAV for calendars, and CardDAV for contacts — each with different authentication, request formats, and data models. This SDK unifies them under a single, consistent Pythonic interface.
- No more hand-rolling XML. The CalDAV and CardDAV clients handle PROPFIND, REPORT, MKCALENDAR, and PROPPATCH requests for you.
- No more manual iCalendar parsing. The
icalmodule handles RFC 5545 line folding, escaping, datetimes, attendees, recurrences, and VALARM reminders. - No more raw dict wrangling. Every API response is parsed into a typed Pydantic model with fields you can autocomplete.
- Drop-in compatible with the Rust CLI. If you already use fastmail-cli, your credential config file works without changes.
🔧 Usage
All clients are async context managers built on
httpx. Create them with async with for
automatic connection lifecycle.
JMAP — Email
Authenticate with a Fastmail API token.
import asyncio
from fastmail_sdk import JmapClient, EmailAddress, SearchFilter
async def main():
async with JmapClient(token="YOUR_API_TOKEN") as client:
await client.authenticate()
# List mailboxes
mailboxes = await client.list_mailboxes()
for m in mailboxes:
print(f"📁 {m.name}: {m.unread_emails} unread")
# Find the inbox and list recent emails
inbox = await client.find_mailbox("inbox")
emails = await client.list_emails(inbox.id, limit=10)
for e in emails:
print(f" {e.subject} ({e.received_at})")
# Send an email
await client.send_email(
to=[EmailAddress(email="friend@example.com", name="Friend")],
subject="Hello from Python 🐍",
body="This was sent programmatically via JMAP.",
)
# Search
results = await client.search_emails(
SearchFilter(text="invoice", unread=True),
mailbox_id=inbox.id,
limit=5,
)
print(f"Found {len(results)} matching emails")
# Get full email with body
if results:
full = await client.get_email(results[0].id)
print(full.text_content())
asyncio.run(main())
More JMAP operations
Reply and forward
# Reply to an email
await client.reply_email(
original=email,
body="Thanks for the update!",
reply_all=True,
)
# Forward an email
await client.forward_email(
original=email,
to=[EmailAddress(email="colleague@example.com")],
body="FYI — see below",
)
Move and mark
archive = await client.find_mailbox("archive")
await client.move_email(email_id, archive.id)
await client.set_keywords(email_id, {"$seen": True})
await client.mark_spam(email_id)
Masked email
masked = await client.create_masked_email(
for_domain="example.com",
description="Newsletter signup",
)
print(f"Created: {masked.email}")
all_masked = await client.list_masked_emails()
for m in all_masked:
print(f"{m.email} — {m.state}")
Download attachments
email = await client.get_email(email_id)
for att in (email.attachments or []):
if att.blob_id:
data = await client.download_blob(att.blob_id)
with open(att.name or "attachment", "wb") as f:
f.write(data)
CalDAV — Calendars
Authenticate with your Fastmail email and an app password. The CalDAV protocol communicates over XML; the client handles all PROPFIND and REPORT requests internally.
import asyncio
from fastmail_sdk import CalDavClient, load_credentials
from fastmail_sdk.models.event import EventQuery
async def main():
username, app_password = load_credentials()
async with CalDavClient(username, app_password) as client:
# List all calendars
calendars = await client.list_calendars()
for cal in calendars:
marker = "⭐" if cal.is_default else " "
print(f"{marker} {cal.name} — {cal.color}")
# List today's events
from fastmail_sdk.ical import default_today_range
start, end = default_today_range()
events = await client.list_events(EventQuery(start=start, end=end))
for event in events:
print(f" {event.title}: {event.start.value} → {event.end.value}")
# Fetch a specific event
event = await client.get_event_by_id("event-uid-here")
print(f"Found: {event.title} in {event.calendar_name}")
asyncio.run(main())
Event CRUD and calendar management
Create, update, and delete events
from fastmail_sdk.models.event import EventCreate, CalendarEvent, EventDateTime
# Create an event
default = await client.default_calendar()
created = await client.create_event(
calendar_id=default.id,
event=CalendarEvent(
id="generate-a-uuid",
calendar_id=default.id,
title="Team Standup",
start=EventDateTime(value="2025-01-15T09:00:00", timezone="America/New_York"),
end=EventDateTime(value="2025-01-15T09:30:00", timezone="America/New_York"),
location="Conference Room B",
attendees=[EventAttendee(email="alice@example.com", name="Alice")],
reminders=[EventReminder(minutes_before=-10)],
),
)
# Update (requires the current ETag)
created.title = "Team Standup (extended)"
updated = await client.update_event(created, created.etag)
# Delete
await client.delete_event(updated)
Calendar management
# Create a new calendar
new_cal = await client.create_calendar("Work Projects", color="#3a87ad")
# Update calendar display name
updated_cal = await client.update_calendar(new_cal, name="Work (Active)")
# Delete a calendar
await client.delete_calendar(updated_cal)
iCalendar round-tripping
from fastmail_sdk.ical import serialize_ical_event, build_event_uid
# Serialize any event to valid iCalendar text
ical_text = serialize_ical_event(event)
print(ical_text)
# Generate a new event UID
uid = build_event_uid()
CardDAV — Contacts
import asyncio
from fastmail_sdk import CardDavClient, load_credentials
from fastmail_sdk.models.contacts import Contact, ContactEmail
async def main():
username, app_password = load_credentials()
async with CardDavClient(username, app_password) as client:
# List address books
books = await client.list_addressbooks()
default_book = books[0].href
# List all contacts
contacts = await client.list_contacts(default_book)
for c in contacts:
emails = ", ".join(e.email for e in c.emails)
print(f"{c.name}: {emails}")
# Search contacts
results = await client.search_contacts("Alice")
for c in results:
print(f"Found: {c.name} — {c.organization}")
asyncio.run(main())
Contact and group management
Create, update, and delete contacts
import uuid
# Create a contact
result = await client.create_contact(
default_book,
contact=Contact(
id=str(uuid.uuid4()),
name="Jane Smith",
emails=[ContactEmail(email="jane@example.com", label="work")],
organization="Acme Corp",
title="Engineer",
),
)
# Update
contact = await client.get_contact_by_id("contact-uid")
contact.notes = "Met at PyCon 2025"
new_etag = await client.update_contact(contact.href, contact.etag, contact)
# Delete
await client.delete_contact(contact.href, contact.etag, contact.id)
Contact groups
from fastmail_sdk.models.contacts import ContactGroup
# List groups
groups = await client.list_groups()
# Create a group
group = ContactGroup(id=str(uuid.uuid4()), name="Project Team")
result = await client.create_group(default_book, group)
# Add and remove members
etag = await client.add_group_member( # Returns new ETag
result.href, result.etag, group, "contact-uid"
)
etag = await client.remove_group_member(
result.href, etag, group, "contact-uid"
)
📚 API Reference
Clients
| Client | Protocol | Authentication | Base URL |
|---|---|---|---|
JmapClient |
JMAP (JSON) | API token | https://api.fastmail.com/jmap/session |
CalDavClient |
CalDAV (XML) | App password | https://caldav.fastmail.com |
CardDavClient |
CardDAV (XML) | App password | https://carddav.fastmail.com |
Models
| Model | Module | Description |
|---|---|---|
Email, Mailbox, Identity, MaskedEmail |
models.email |
JMAP email domain objects |
Calendar, CalendarEvent, EventQuery |
models.calendar, models.event |
CalDAV calendar domain objects |
Contact, ContactGroup, AddressBook |
models.contacts |
CardDAV contact domain objects |
Error Types
| Exception | When |
|---|---|
FastmailError |
Base exception — all SDK errors inherit from this |
NotAuthenticated |
Missing or invalid credentials |
CalendarNotFound / EventNotFound |
Requested resource doesn't exist |
CalendarConflict / EventConflict |
ETag mismatch — someone else modified the resource |
Utilities
| Function | Description |
|---|---|
load_credentials() |
Read username/app_password from env or config file |
parse_ical_event() |
Parse iCalendar text into a CalendarEvent |
serialize_ical_event() |
Serialize a CalendarEvent to iCalendar text |
build_event_uid() |
Generate a UUID v4 for new events |
default_today_range() / current_week_range() |
Convenience date ranges for event queries |
🤝 Contributing
We welcome contributions! See CONTRIBUTING.md for development setup, code style, and the pull request process.
Tests use pytest with respx for HTTP mocking — no Fastmail account required:
uv run pytest
This project follows the Contributor Covenant code of conduct.
💬 Getting Help
- GitHub Issues — bug reports and feature requests
- GitHub Discussions — questions, ideas, and community chat
- SECURITY.md — vulnerability reporting (private channel)
📄 License
MIT © 2025 The fastmail-py-sdk Authors. See LICENSE for details.
🙏 Acknowledgements
- fastmail-cli — the Rust CLI whose battle-tested logic this SDK ports
- httpx — the async HTTP library powering all three clients
- Pydantic — the data validation layer behind every domain model
- icalendar — used as a structural parsing fallback in the iCalendar module
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 fastmail_py_sdk-0.1.0.tar.gz.
File metadata
- Download URL: fastmail_py_sdk-0.1.0.tar.gz
- Upload date:
- Size: 55.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.0.1 CPython/3.12.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8c9a4f538c3af8a61b86f4adf6faf2992b8f3811240c924ed8e81bc962d21868
|
|
| MD5 |
95de1a53ba6b86e72f77f380f350dab5
|
|
| BLAKE2b-256 |
3fc5ff248e64fcf54e9e25bea3edb8a265f5773913bf4e2e0a353bc7acd816ac
|
Provenance
The following attestation bundles were made for fastmail_py_sdk-0.1.0.tar.gz:
Publisher:
publish.yml on kerryhatcher/fastmail-py-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fastmail_py_sdk-0.1.0.tar.gz -
Subject digest:
8c9a4f538c3af8a61b86f4adf6faf2992b8f3811240c924ed8e81bc962d21868 - Sigstore transparency entry: 2222075133
- Sigstore integration time:
-
Permalink:
kerryhatcher/fastmail-py-sdk@441b30b6482984c2cf3f06fa8f8154992d66caa9 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/kerryhatcher
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@441b30b6482984c2cf3f06fa8f8154992d66caa9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file fastmail_py_sdk-0.1.0-py3-none-any.whl.
File metadata
- Download URL: fastmail_py_sdk-0.1.0-py3-none-any.whl
- Upload date:
- Size: 38.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.0.1 CPython/3.12.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c7b74e9b580fb7c67efd251a97ec38f7a51776702441f7241956e8425098d853
|
|
| MD5 |
69274deb71b5141ad7bb88866a75d43e
|
|
| BLAKE2b-256 |
3bc96e471e346e20d0b7793210a132aec30d7eb44e93834214c41cd501b9520d
|
Provenance
The following attestation bundles were made for fastmail_py_sdk-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on kerryhatcher/fastmail-py-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fastmail_py_sdk-0.1.0-py3-none-any.whl -
Subject digest:
c7b74e9b580fb7c67efd251a97ec38f7a51776702441f7241956e8425098d853 - Sigstore transparency entry: 2222075792
- Sigstore integration time:
-
Permalink:
kerryhatcher/fastmail-py-sdk@441b30b6482984c2cf3f06fa8f8154992d66caa9 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/kerryhatcher
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@441b30b6482984c2cf3f06fa8f8154992d66caa9 -
Trigger Event:
push
-
Statement type: