Files
mailflare/docs/api.md
T
Oday BakkourandClaude Fable 5.1 3655f616be feat(jmap): JMAP server for external mail clients
Adds a JMAP (RFC 8620 core, RFC 8621 mail, submission) endpoint so mail
apps such as Mailtemi, Twake Mail and aerc can read and send Mailflare
mail. Discovery at /.well-known/jmap, session at /jmap/session, API at
/jmap/api, blobs at /jmap/download and /jmap/upload, push at
/jmap/eventsource.

- Auth: an API key with the new `jmap` scope, as a Bearer token or as the
  password of HTTP Basic auth. `authenticateApiKeyValue` is split out of
  the Next-bound `src/lib/api/auth.ts` into a framework-free module and
  refreshes `lastUsedAt` at most once a minute, since protocol clients make
  many small calls.
- Model: the account is the user. Each Mailflare mailbox is a top-level
  JMAP Mailbox with six system children keyed by status and one child per
  user folder; ids encode `mailboxId`, `mailboxId~role` or
  `mailboxId~f~folderId`. A message is in exactly one of them.
- Methods: Mailbox/get|query|set (folders only), Thread/get,
  Email/get|query|set, SearchSnippet/get, Identity/get,
  EmailSubmission/set, Core/echo. Every */changes answers
  cannotCalculateChanges (messages have no change log), so clients
  re-query when the state string moves; eventsource emits StateChange by
  polling the state digest.
- Email/query maps FilterCondition and AND/OR/NOT trees onto message
  columns, supports collapseThreads, anchors, position and calculateTotal.
  Email/get builds objects from stored rows (no MIME parsing), with
  bodyValues, bodyStructure, attachments as blob ids, and header:X forms
  answered from stored headers.
- Email/set updates $seen/$flagged, moves via mailboxIds or
  mailboxIds/<id> patches, creates drafts (with uploaded attachments), and
  destroys to Trash first and permanently from Trash.
  EmailSubmission/set sends a draft through the existing send path and
  reports it destroyed via the implicit Email/set response, since the sent
  copy is a new row. Result references (#ids with resultOf/name/path,
  including the * form) and #creationIds work within a request.
- Settings > Account > Email apps mints a jmap-scoped key and shows the
  server details once. docs/api.md and CLAUDE.md describe the mapping.

Verified with a 47-check script on local D1: discovery redirect, 401/403
paths, Basic and Bearer auth, capabilities, unknown method/capability
errors, mailbox tree and counts, folder create/rename/destroy and system
mailbox protection, query with collapseThreads and result references,
attachment/text/operator filters, bodies with truncation, blob download
of an attachment and of a raw message, Thread/get, snippets, keyword and
mailbox patches, ifInState, upload, Identity/get, a draft created and
submitted in one request with #d1, the sent copy carrying cc and the
attachment, two-step destroy, and eventsource. tsc (11) and eslint (105)
unchanged from main.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 15:32:47 +03:00

5.0 KiB

API and integrations

Mailflare exposes APIs for domain management and sending email. Authentication and mailbox permissions still apply to these routes.

Domain management

Adding or removing a domain from Mailflare also updates Cloudflare Email Routing and sending resources.

Mailflare route Purpose
GET /api/domains List connected domains
POST /api/domains Connect a domain and configure Cloudflare
GET /api/domains/[id] Get a connected domain
DELETE /api/domains/[id] Remove a domain and clean up its Cloudflare resources
GET /api/domains/[id]/dns View its routing and sending DNS status

The hostname must be the apex of a zone available to the configured Cloudflare credentials, or a subdomain of that zone. Creating a mailbox also creates the Cloudflare Email Routing rule that delivers its address to the mailflare Worker.

Sending email

Send email through POST /api/v1/send. to, cc and bcc accept either a comma-separated header string or an array of addresses; each entry may carry a display name ("Maya Chen" <maya@example.net>). A message can reach up to 50 recipients across the three fields. Attachments are optional and use Base64-encoded content:

{
  "from": "support@example.com",
  "to": ["user@example.net", "\"Maya Chen\" <maya@example.net>"],
  "cc": "ops@example.com",
  "bcc": ["audit@example.com"],
  "subject": "Report",
  "text": "Attached.",
  "attachments": [
    {
      "filename": "report.pdf",
      "type": "application/pdf",
      "contentBase64": "<base64 data>"
    }
  ]
}

To send a reply that threads correctly in the recipient's client, pass the parent's Message-ID as inReplyTo and its chain as references (a header string or an array). Mailflare writes the In-Reply-To and References headers, files the sent copy in the same conversation, and stores threadId, inReplyTo and references on every message.

{
  "from": "support@example.com",
  "to": "user@example.net",
  "subject": "Re: Report",
  "text": "Thanks, received.",
  "inReplyTo": "<CAF1abc@mail.example.net>",
  "references": ["<CAF0root@mail.example.net>", "<CAF1abc@mail.example.net>"]
}

GET /api/messages/{id}/thread (session auth) returns every stored message in the same conversation, oldest first, excluding drafts and trash. GET /api/messages?group=thread collapses a list to one row per conversation (its newest message matching the filter) and adds threadCount, threadUnread and threadMessageIds, the ids that row stands for within the current filter, so bulk actions can act on the whole conversation.

Messages composed in Mailflare are sent as HTML with a plain-text alternative derived from it. Quoted or forwarded content is wrapped in <div class="mailflare-quote" data-mailflare-quote="1"> so the reader can fold it. POST /api/drafts accepts forwardOfMessageId, which copies that message's attachments onto the new draft; DELETE /api/drafts/{id}/attachments/{attachmentId} removes one, and POST /api/send with draftId sends the draft's stored files along with any uploaded in the request.

The dashboard composer accepts up to 10 attachments, with a 10 MB limit per file and a 20 MB combined limit. Attachment metadata is stored in D1 and file content is stored in R2. Downloads require access to the mailbox containing the message.

JMAP

Mailflare serves JMAP (RFC 8620 core and RFC 8621 mail, plus submission) so external mail apps can read and send mail. Discovery is at /.well-known/jmap, which redirects to /jmap/session. Authenticate with an API key that has the jmap scope, either as Authorization: Bearer <key> or as the password of HTTP Basic auth (the username is ignored). Settings > Account > Email apps mints such a key.

The account id is the user id. Each Mailflare mailbox appears as a top-level JMAP Mailbox with system children (inbox, drafts, sent, archive, junk, trash) and one child per user folder; a message belongs to exactly one of them. Supported methods: Mailbox/get|query|set (folders only), Thread/get, Email/get|query|set, SearchSnippet/get, Identity/get, EmailSubmission/set, and Core/echo. */changes return cannotCalculateChanges, so clients re-query on a state change; /jmap/eventsource pushes state changes by polling. Email/set creates drafts, updates $seen and $flagged, moves between mailboxes, and destroys (to Trash first, then permanently). EmailSubmission/set sends a draft and reports it destroyed, since the sent copy is a new message. Blob download and upload follow the Session's downloadUrl and uploadUrl.

Real-time updates

Mailflare uses a Durable Object WebSocket hub to notify connected users after an inbound message is stored. Mailbox owners, the domain administrator, and delegated users receive events for mailboxes they can access.

The REALTIME binding and its migration are declared in wrangler.jsonc. When a WebSocket is temporarily unavailable, the app retries the connection and uses a slower refresh until it recovers.