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
65 lines
3.7 KiB
Markdown
65 lines
3.7 KiB
Markdown
# 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:
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
{
|
|
"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.
|