# Security & limits

How Mail HQ protects personal email, how to operate it safely, and what it does not do yet.

Canonical URL: https://kb.byaditya.com/entry/docs/mail-hq/security
Source: https://docs.adityabaindur.com/docs/mail-hq/security
Source updated: 2026-10-08T01:29:22Z
Indexed: 2026-11-05T03:56:29.576Z

---

## 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-store` caching, `nosniff`, `no-referrer`, and `X-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

**warn**

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.

