Documentation
Security & limits
How Mail HQ protects personal email, how to operate it safely, and what it does not do yet.
Overview
Mail HQ serves personal email. Keep the dashboard behind Cloudflare Access and the R2 bucket private.
Dashboard
Cloudflare Access login, verified again by the Worker on every request
Agents
Hashed, expiring, revocable tokens scoped to one mailbox
Storage
Private R2 for mail content; encrypted provisioning credential
Dashboard authentication
- The Worker validates the Access application JWT itself: signature, issuer, audience, expiry, and user-session claims, independently of the edge policy.
- Requests must arrive on the exact
APP_ORIGIN, and mutations from another origin are rejected. - Access policies decide who can use the dashboard. There is no second hardcoded email allowlist.
- Dashboard responses are sent with
no-storecaching,nosniff,no-referrer, andX-Frame-Options: DENY.
Agent tokens
MCP tokens are independent, random, hashed at rest, limited to one mailbox, scoped to read and/or send, expiring, and revocable. A read-only token does not authorize changing mailbox state, and read_mail does not mark messages as read. Never publish tokens or put them in URL query strings.
Provisioning credential
The optional Cloudflare provisioning token is AES-GCM encrypted in R2 with CONFIG_ENCRYPTION_KEY. Keep that key stable and backed up privately, and restrict the token to the intended zones and permissions. No normal app endpoint overwrites existing MX records or email routing rules.
Untrusted mail
Received mail is untrusted input, including when it is returned to an agent over MCP. Inbound HTML is shown in a sandboxed frame with scripts, forms, and remote resources blocked. Review agent permissions before allowing an agent to send.
Sending and delivery
| Status | Meaning |
|---|---|
| Sent to Cloudflare | The provider accepted the submission. Not proof of delivery or inbox placement |
| Failed | A definitive rejection was recorded |
| Uncertain | Submission may have happened but confirmation was lost. Check before retrying |
Retries of a send must reuse its idempotency key. Ambiguous failures are not retried automatically, and a temporary SMTP 4xx response can be retried by the provider, so do not submit another copy yourself. A provider-accepted message may still bounce or be filtered into spam.
When investigating placement, read the recipient's original headers (SPF, DKIM, DMARC alignment, TLS, envelope sender, Message-ID, Date, MIME structure) and check Cloudflare sending events for deferrals or bounces. Passing SPF, DKIM, and DMARC does not guarantee the inbox.
Backups
Retain database and object-storage backups together: message metadata in D1 refers to bodies and attachments in R2 by object key. Rolling back Worker code does not roll back D1 or R2. Trash is reversible, and there is no automatic permanent-deletion job.
Keep real email samples, account records, private keys, environment files, generated deployment bundles, and test-recipient logs out of Git.
Current limits
| Limit | Value |
|---|---|
| Outgoing email size | 5 MiB, including attachment encoding |
| Recipients per message | 50 combined (to, cc, bcc) |
| Dashboard attachment upload | 3 MiB per attachment |
| Incoming email size | 25 MiB |
| MCP attachment download | 2 MiB |
| MCP requests | 120/min per token, 240/min per client IP |
| Sends | 30/min per mailbox |
| Agent token lifetime | 1–365 days (dashboard offers 7, 30, 90, 365) |
Not supported yet
- Search covers subject, sender, and preview text only.
- External historical mail is not imported automatically.
- IMAP/SMTP access is planned, not implemented.
- Push notifications and offline support are not implemented.