Supermail
API reference

Mutations and sending

Modify provider state, manage drafts and mailboxes, and create durable Sync submissions from RFC 5322 messages.

Writes are intent-shaped: an app says archive, move, mark spam, or send and Supermail translates that intent to classical IMAP/SMTP, Gmail, or Microsoft Graph. Every call is double-gated by the live grant and the account's working capability.

Staged content

Draft and send bytes never enter JSON-RPC or Gateway storage. Upload the exact RFC 5322 bytes first:

POST /gateway/content?account_id=account_01H9X...&purpose=send
Authorization: Bearer <access_token>
Content-Type: message/rfc822

From: me@example.com
To: you@example.net
Message-ID: <compose-42@example.com>

Hello.
201 Created
{ "upload_ref": "up_01H..." }

Use purpose=modify for Drafts/put (mail.modify) and purpose=send for MessageSubmissions/create (mail.send). Handles are account-bound, short-lived, and deleted when a submission reaches a terminal state. The send upload must contain an RFC 5322 Message-ID header; it is the crash-ambiguity check that prevents a second provider effect.

Message mutations

All require mail.modify plus working.modify on the account.

MethodRequest
Messages/set_flagsaccount_id, provider_id, at least one of seen / flagged
Messages/patch_labelsaccount_id, provider_id, add and remove user-label ID arrays
Messages/archive / unarchiveaccount_id, provider_id
Messages/trash / restoreaccount_id, provider_id
Messages/mark_spam / unmark_spamaccount_id, provider_id
Messages/deleteaccount_id, provider_id
Messages/moveaccount_id, provider_id, folder_id

Every message mutation requires an idempotency_key. Retry the same intended action with the same key; a changed target or payload with that key is refused. A later deliberate action gets a new key. provider_id is the opaque ProviderId returned by email events.

patch_labels supports Gmail custom labels independently of message location. Each array admits up to 100 exact label IDs; either may be empty, their union must be nonempty, and duplicates or overlap are refused. System labels retain the dedicated read/star/archive/spam/trash actions. The native adapter validates the labels under the same account; draft targets are refused.

Labels/create refuses a name the provider cannot store with label_name_unsupported, for example an IMAP name that maps to a reserved keyword ($…, Junk), a Gmail system label's name (Sent, Chats, in any case) or an Outlook category name with a comma (on the master list also a semicolon). A name another Gmail label or Outlook category already has is refused with label_name_taken. A color outside the account's scheme (for Gmail, outside its label palette) is refused with label_color_unsupported before any request. The refusal is permanent: retrying the same name cannot succeed, so keep such a label in your own store instead. Timeouts and rate limits are transient and may be retried.

IMAP stores a label as the keyword Thunderbird derives for a tag of that name: letters, digits and safe symbols stay, every other UTF-8 byte becomes =xx, the keyword is lowercased, and one over 50 characters becomes &x-moz- and a 128-bit SHA-256 prefix. "Project X" is stored as project=20x; the created label's id is the keyword.

Each call returns the original Operation. RETRYING retains a transient preflight failure before any provider write; the same operation retries with backoff. RUNNING means execution is in progress, UNCERTAIN means the provider outcome is being established automatically, and RECONCILING means the provider confirmed the action while Inbox convergence is pending. AWAITING_OWNER means automatic resolution is exhausted: owner_decision names the typed decision the mailbox owner makes; Supermail never repeats the write on its own. Every operation carries a positive version that owner decisions name as expected_version. SUCCEEDED, FAILED, and CANCELED are terminal. Use Operations/get to refresh status, or Operations/list filtered by idempotency_key to recover a lost acceptance response. An uncertain action must never be replaced silently with a fresh key.

Owner decisions

There is no operator. Supermail resolves a write whose outcome is unknown by itself: it reads the target back, applies the write again when the readback proves it was not applied (every mail change sets a state, so this is safe), and re-syncs the account from the provider before a final readback. Only what stays unknowable after that becomes an owner decision, and nobody but the mailbox owner is ever asked.

owner_decision.kindDecided byChoices
review_change (with evidence and retry_available)Operations/decideretry (only when retry_available) or dismiss
confirm_delivery (with submission_id)MessageSubmissions/decide_deliverysend_again, already_sent or discard
resolve_sent_copy (with submission_id and cause)MessageSubmissions/resolve_filingretry or abandon
repair_account (with cause)Accounts/repairRebuild the account's local copy from the provider

Every decision request carries an idempotency_key and the expected_version the owner saw. A replay with the same key returns the first answer. A stale version is refused with STATE_MISMATCH, and that refusal is replayed too. A transient failure (rate limit, unavailable dependency, lost reply) binds nothing, so the same request may be sent again. Operations/list and MessageSubmissions/list accept an optional statuses filter, for example OPERATION_STATUS_AWAITING_OWNER to collect every pending decision.

retry applies the change again on the owner's authority. dismiss keeps the mailbox as the provider shows it and cancels the operation. Accounts/repair rebuilds the account's local copy from the provider; credentials, drafts and submissions are kept, and the repair settles the account_repair operation.

Keys are account-wide across operation kinds. Mail requests may carry a source_scope containing the exact original stream_id and lineage_id. Fresh admission refuses a replaced or closed source; request-bound replay of an already accepted operation remains stable. Inbox always retains this condition with its original local-ID request and native translation.

Relocation results retain a provider-acknowledged successor message identity when available. Inbox observes current visibility separately from provider success; an operation cursor is not an Inbox applied-Commit receipt. Missing native correlation remains explicit and allows refreshing or safely dismissing the completed action, without guessing from a name or Message-ID.

Drafts

Draft methods require mail.modify plus working.modify.

MethodRequest
Drafts/putaccount_id, upload_ref from a purpose=modify upload; optional draft_id and expected_revision to replace an existing draft
Drafts/getaccount_id, draft_id
Drafts/listaccount_id; optional page (size, token)
Drafts/deleteaccount_id, draft_id, expected_revision

Drafts/put and Drafts/delete accept an optional idempotency_key and return an Operation.

Mailbox mutations

Mailboxes/create, Mailboxes/rename, and Mailboxes/delete require mail.modify and working.modify. Create takes account_id + name; rename takes account_id + folder_id + name; delete takes account_id + folder_id. Each requires an idempotency_key and returns an Operation. Mailboxes/list reads an account's current folders.

Sending

After a send-purpose upload, create a durable submission:

JSON-RPC
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "MessageSubmissions/create",
  "params": {
    "account_id": "account_01H9X...",
    "upload_ref": "up_01H...",
    "idempotency_key": "compose-42"
  }
}

Requires mail.send plus working.send. The idempotency key is required and scoped to the account: replaying it returns the same sub_ row and never creates a second submission. To send an existing draft instead, call MessageSubmissions/create_from_draft with account_id, draft_id, expected_revision, and idempotency_key.

The result is a MessageSubmission whose initial status is MESSAGE_SUBMISSION_STATUS_ACCEPTED or MESSAGE_SUBMISSION_STATUS_TRANSMITTING. Completion appends exactly one email.sent or email.send_failed event, and each status change appends a message_submission.changed event.

Submission status

Call MessageSubmissions/get with account_id and submission_id. It requires mail.send, the grant must still cover the submission's account, and that account must still have a working send capability. MessageSubmissions/list pages through an account's submissions, and MessageSubmissions/cancel takes the same account_id + submission_id pair.

Terminal states are MESSAGE_SUBMISSION_STATUS_PROVIDER_ACCEPTED, MESSAGE_SUBMISSION_STATUS_FAILED (with failure), MESSAGE_SUBMISSION_STATUS_CANCELED, and MESSAGE_SUBMISSION_STATUS_RESENT. Treat non-terminal states as pending and retry get; retrying create with the same idempotency key is also safe.

A lost provider reply never causes another send. MESSAGE_SUBMISSION_STATUS_UNCERTAIN means Supermail is searching the provider's Sent folder for the message's Message-ID; a unique copy confirms the send. When the searches cannot confirm it, the status becomes MESSAGE_SUBMISSION_STATUS_AWAITING_SENDER_DECISION with delivery_evidence: when the send was attempted, why its outcome is unknown, and every search with its time and result. A daily search continues for a week and confirms the send by itself if a copy appears. The sender decides with MessageSubmissions/decide_delivery:

  • send_again creates a new submission with byte-identical content and the same Message-ID; some recipients may receive the message twice. The original becomes MESSAGE_SUBMISSION_STATUS_RESENT with superseded_by, and the new submission names it in retransmission_of. The response carries both as submission and successor, and MessageSubmissions/get reads the successor with the same grant. The original's operation ends OPERATION_STATUS_CANCELED with its target set to the successor (type message_submission); the successor's own operation reports the delivery from then on.
  • already_sent marks it sent.
  • discard cancels it.

While a Sent search of that delivery is running, a decision is refused with RETRY_LATER and its key stays unbound; the same request succeeds once the search settles (unless the search found the message, which delivers it).

Errors

ReasonMeaning
NOT_SUPPORTEDThe account profile/provider cannot perform the verb.
UPLOAD_REF_STALEThe staged handle expired, belongs to another account, or was consumed. Upload again.
SEND_REJECTEDThe provider permanently refused the message. Fix the message rather than retrying unchanged.

A missing grant scope is PERMISSION_DENIED; a scope-valid write whose account capability is currently down is FAILED_PRECONDITION.

On this page