Skip to content

Email setup

This page is about desk mail: the organization’s support mailbox talking to customers. For instance mail — Kiku talking to its own agents, admins and invitees — see the configuration reference.

Kiku sends and receives from a subdomain of a domain you own — help.example.com — never the root. That one decision is why your existing mail is never touched, why Microsoft 365 and Google Workspace need no OAuth app, and why there are four DNS records to create.

Everything on this page is Settings → Email in the app; none of it is an environment variable, and there is no env fallback for any of it.

The four DNS records

All on the support subdomain. The values come from whoever hosts the mailbox and whoever relays your outbound mail — this table is the shape, not the contents.

RecordNamePurpose
MXhelp.example.comwhere inbound mail for the support address is delivered
TXT (SPF)help.example.comauthorizes your relay to send as the subdomain
TXT (DKIM)<selector>._domainkey.help.example.comthe signing key your relay gives you
TXT (DMARC)_dmarc.help.example.compolicy for what receivers do when the two above fail

Start DMARC at p=none with a rua= address and tighten once you have seen a week of reports. Publishing p=reject on day one against an SPF record you have not verified is how a support inbox goes quiet without anyone noticing.

Give propagation an hour before you conclude anything is broken.

Inbound: IMAP is the default

Settings → Email → Inbound, mode IMAP polling. It needs no public ingress — an outbound connection only, so it works behind NAT, behind Tailscale, on a dynamic IP, with no port open at all.

FieldNote
Support addressmust be on the subdomain
IMAP host / port / user / passwordport 993 with TLS is the default
Poll interval60 seconds by default

Only INBOX is polled, and that is not configurable. Connection test proves the credentials before you save your way into a silent inbox.

If your mailbox provider is Microsoft 365 or Google Workspace and the account is on the root domain, see Coming from Microsoft 365 below.

The plus-addressing probe

Kiku’s most robust threading layer puts reply+<signed-token>@help.example.com in Reply-To, because the recipient address is the one thing mail clients essentially never rewrite. That only works if your mailbox actually delivers plus-addressed mail — it needs plus-addressing or a catch-all, and plenty of mailboxes have neither.

So setup sends itself a probe rather than assuming: a message to a plus-addressed variant of your own support address, and then it watches the polled mailbox for it. The verdict is stored and shown in Settings → Email.

  • Not verified — no probe has run yet. Threading layer 1 is off until one does.
  • Verified — the probe came back. Threading layer 1 is on.
  • Waiting — sent, not yet seen. The window is three poll intervals, minimum five minutes.
  • Failed — nothing arrived in that window. This is not an error you have to fix. Layer 1 self-disables and threading falls to the three layers below it: a signed token inside our own Message-ID matched through In-Reply-To and References, a hidden marker in the message body (which is what survives a forward), and optionally a [#1234] subject token, off by default. Replies still thread. You lose the sturdiest rung, not the ladder.
  • Assumed — you are on the webhook path, where the subdomain’s MX is ours and a catch-all is how mail arrives at all. Nothing to verify.

If Failed bothers you, turn on plus-addressing or a catch-all at the mailbox and re-run the probe. It is a button, not a reinstall.

Outbound: SMTP

One SMTP block, and no outbound mode selector — SMTP is the only outbound path in v1 (ADR 0004). Host, port, user, password, and the From display name. Port 587 with implicit TLS off is the default, because that is what submission uses: the relay answers in plaintext and the connection is upgraded with STARTTLS. 465 is the implicit-TLS port.

Sent mail comes from the support address on the subdomain, which is what the SPF and DKIM records above are for.

The webhook path

The production alternative to polling: sub-second delivery, and the provider retries and buffers for you. It needs a URL your provider can reach.

Settings → Email → Inbound, mode Webhook, pick your provider, save. Kiku generates the signing secret on that first save — it is never accepted from the browser — and shows it next to the endpoint URL, https://your-instance/api/email/inbound/<organization-id>. Point the provider’s inbound route at that endpoint carrying the secret, as an Authorization: Bearer header or as ?secret= on the URL.

While the instance is in IMAP mode the route answers 503. Set the mode first.

Amazon SES must use the S3 receipt action, not SNS. The SNS action caps the entire message at 150 KB, which is unusable for real support mail, and there is no truncated mode — so a small function reads the S3 object and posts its bytes. Kiku holds no AWS credentials and cannot fetch the object itself; a notification carrying only the pointer is quarantined saying exactly that.

Per-provider setup — Postmark’s raw-content toggle, Mailgun’s /mime suffix, the Cloudflare Email Worker, the SES shape, and every failure code — is in Inbound mail over the webhook.

Coming from Microsoft 365

A company already running support@example.com on M365 with years of history cannot plug that mailbox in directly: Microsoft removed IMAP basic auth entirely, and Kiku ships no Entra OAuth app.

The migration path is a forwarding rule: leave the M365 mailbox where it is, and forward it into the address on your Kiku subdomain. No OAuth, no tenant changes, no app registration — one rule. Your old mail stays in M365; new mail becomes tickets. Forwarded mail arrives with an envelope sender that differs from the original sender, which the pipeline handles.

The same applies to Google Workspace.

Size and quarantine

Inbound messages are capped at 25 MB on both paths, measured on the decoded message. It is not configurable. Over the cap, or unparseable, or arriving under a setting that cannot decode it: the message goes to quarantine with its raw bytes retained, so a corrected setting can replay it. Nothing is silently dropped.