Skip to main content

Python SDK for Fastmail's CalDAV API — calendars, events, and iCalendar round-tripping

Project description

fastmail-py-sdk

License CI PyPI Python 3.13+

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 icalendar library 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-cli config file (~/.config/fastmail-cli/config.toml)
  • 🧪 Testedrespx-powered HTTP mocking ensures the full test suite runs without a Fastmail account
  • 🐍 Fully typedPydantic v2 models with camelCase aliases 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 ical module 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

📄 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fastmail_py_sdk-0.1.0.tar.gz (55.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastmail_py_sdk-0.1.0-py3-none-any.whl (38.2 kB view details)

Uploaded Python 3

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

Hashes for fastmail_py_sdk-0.1.0.tar.gz
Algorithm Hash digest
SHA256 8c9a4f538c3af8a61b86f4adf6faf2992b8f3811240c924ed8e81bc962d21868
MD5 95de1a53ba6b86e72f77f380f350dab5
BLAKE2b-256 3fc5ff248e64fcf54e9e25bea3edb8a265f5773913bf4e2e0a353bc7acd816ac

See more details on using hashes here.

Provenance

The following attestation bundles were made for fastmail_py_sdk-0.1.0.tar.gz:

Publisher: publish.yml on kerryhatcher/fastmail-py-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file fastmail_py_sdk-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for fastmail_py_sdk-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c7b74e9b580fb7c67efd251a97ec38f7a51776702441f7241956e8425098d853
MD5 69274deb71b5141ad7bb88866a75d43e
BLAKE2b-256 3bc96e471e346e20d0b7793210a132aec30d7eb44e93834214c41cd501b9520d

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page