Skip to content

Repository files navigation

fastmail-py-sdk — Async Python SDK for Fastmail: email, calendars, contacts

CI PyPI version Python versions License: MIT Linted with Ruff

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

About

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

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages