Files
mailflare/docs/api.md
T
Oday BakkourandClaude Fable 5.1 c6a38ae8e5 feat(mail): conversation view, forwarding with attachments, rich-text composer
Completes the three follow-ups from the threading PR.

Conversation view
- `GET /api/messages?group=thread` returns one row per conversation (its
  newest message matching the filter) via a latest-per-thread subquery,
  with `threadCount`, `threadUnread`, and `threadMessageIds` (the ids the
  row stands for within the current filter).
- Lists default to conversation view, remembered per browser, with a header
  toggle. Row and bulk actions, drags, and selection expand to the whole
  conversation; a row is bold while any message in it is unread. Drafts are
  never grouped.

Forwarding
- Forward builds a "---------- Forwarded message ---------" block from the
  original and creates a draft with `forwardOfMessageId`; the server copies
  the original's attachments onto the draft (new R2 objects, since keys are
  unique per row). The composer shows them as removable chips.
- `/api/send` accepts `draftId` and sends the draft's stored files with
  whatever was uploaded in the request. Deleting a draft now removes its R2
  objects too, and `DELETE /api/drafts/[id]/attachments/[attachmentId]`
  drops one file.

Rich-text composer
- contentEditable editor with bold/italic/underline/strikethrough, lists,
  quote, link (⌘K) and clear formatting; pasted content is flattened to text.
- The body is one HTML string; the text/plain part is derived from it, with
  bullets, numbering and "> " quote prefixes so text-only clients and the
  reader's collapse heuristics still work.
- Reply and forward quotes are HTML wrapped in a marked block that both the
  composer (•••) and the reader ("Quoted text") fold, so rich replies keep
  their formatting instead of falling back to text. Signature handling and
  draft round-trips work on the HTML.
- Shared `.email-body` styles restore list markers and blockquotes that
  Tailwind preflight strips from rendered mail.

Verified on a fresh local D1: grouped inbox collapses 4 messages to 1 row
with the right ids; a forward from an imported message carried its PDF onto
the draft, survived a draft reload, sent with the file, and joined the
original's thread; deleting the draft dropped its R2 object. tsc (11) and
eslint (96) unchanged from baseline.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGeWDVSkXtVEmDAN2AtB9h
2026-09-06 15:13:40 +03:00

3.7 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.

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.