Email: domains, mailboxes & sending

Tandem gives an app three ways to do email:

  • Real mailboxes — full IMAP + SMTP + JMAP accounts (send, receive, store).
  • Transactional send API — a Resend-style HTTP lane for sending mail with no mailbox.
  • Inbound webhooks — receive a mailbox’s mail as HTTP POSTs instead of over IMAP.

Email is custom-domain-only: every mailbox and every From address lives on a domain the org owns, and that domain must be onboarded first.

Step 1 — Onboard a domain

{ "name": "add_email_domain", "arguments": { "projectId": "…", "domain": "example.com" } }

add_email_domain registers an AWS SES sending identity, tells the mail server to accept the domain, and returns the DNS records to publish: MX (inbound), plus DKIM CNAMEs, SPF, and DMARC (outbound). If the domain’s DNS is managed by Tandem, the records are created automatically; otherwise the response gives a copy-paste record list for the tenant’s DNS provider.

Then verify:

{ "name": "verify_email_domain", "arguments": { "domainId": "…" } }

verify_email_domain re-checks SES (identity + DKIM) and DNS (inbound MX). Mail only sends/receives once the domain is fully verified; if something is still failing the response re-lists the records to add. list_email_domains shows the status of every onboarded domain.

Step 2a — Provision a mailbox

{ "name": "provision_email",
  "arguments": { "projectId": "…", "domain": "example.com", "localPart": "hello", "attachServiceId": "…" } }

This creates a real mailbox (hello@example.com) on the self-hosted mail server and, when attachServiceId is set, injects the EMAIL_* runtime env vars (IMAP + SMTP + JMAP) on that service.

  • attach_email_to_service / detach_email_from_service inject or remove the EMAIL_* vars on any service (idempotent).
  • reveal_email_credentials returns the full connection profile (IMAP/SMTP host/port/secure/username/password + JMAP url/token) for manual setup in a mail app. Audit-logged.
  • rotate_email_credentials sets or auto-generates a new password and re-injects it into every attached service.
  • list_emails lists mailboxes with address, DNS status, and attachments (never passwords).

Step 2b — Or use the transactional send API (no mailbox)

Create a key and send over HTTP:

{ "name": "create_email_api_key",
  "arguments": { "projectId": "…", "name": "app-sender", "allowedDomains": ["example.com"] } }

The key is org-scoped, returned once (store it now), and optionally restricted to specific From domains via allowedDomains. Use it as a Bearer token:

curl -s https://portal.launchtandem.com/api/email/send \
  -H "Authorization: Bearer <the-key>" \
  -H "Content-Type: application/json" \
  -d '{ "from": "hello@example.com", "to": "user@dest.com",
        "subject": "Hi", "html": "<p>Hello</p>" }'

The From domain must be onboarded first. list_email_api_keys lists keys (name, prefix, allowed domains, last used, revoked) without the secret; revoke_email_api_key disables one immediately.

Step 3 — Receive mail over HTTP (inbound webhooks)

Instead of polling IMAP, have Tandem POST parsed mail to your app:

{ "name": "set_email_inbound_webhook",
  "arguments": { "mailboxId": "…", "url": "https://app.example.com/inbound" } }

When mail arrives, the platform POSTs a JSON copy:

{ "type": "email.inbound", "mailbox": "hello@example.com",
  "message": { "from": "…", "to": "…", "cc": "…", "subject": "…",
               "text": "…", "html": "…", "messageId": "…",
               "receivedAt": "…", "attachments": [] } }

set_email_inbound_webhook returns a signing secret once — verify the X-Tandem-Signature header (sha256=<hex HMAC-SHA256 of the raw body>) with it. get_email_inbound_webhook shows the config (secret never returned; call set again to rotate). remove_email_inbound_webhook stops forwarding — mail still lands in the mailbox.

Tearing down

  • remove_email_domain deletes the SES identity and Tandem-managed DNS. It refuses while any mailboxes still exist on the domain — delete those first.
  • delete_email deletes a mailbox (all stored mail is lost), removes the EMAIL_* vars from attached services, and tears down the domain’s DNS if it was the last mailbox. No undo. See Deleting resources.