arizuko › components › emaid
emaid
What it is
In plain terms, emaid is the connector that plugs email into arizuko, so the agent can read incoming mail and send replies in the same thread.
emaid is the email channel adapter. It polls IMAP for inbound messages, sends outbound via SMTP, and keeps a local SQLite table mapping email threads (Message-ID + References) to their chat_jid so replies preserve the conversation. From routd’s point of view it is a normal channel adapter; the JID prefix is email:.
Each inbound message is verified against DMARC results in the upstream MTA’s Authentication-Results header before it reaches the agent. Verified mail rides verb=message; the rest rides verb=untrusted so route rules can quarantine it.
Why it exists
Email is the lowest-common-denominator inbound: every customer, every vendor, every ticket system can reach an arizuko agent if it has an SMTP address. Without emaid there is no IMAP poll loop, no thread table, no sender authentication — the agent has no way to receive email at all.
The split between verb=message and verb=untrusted exists because unauthenticated email is a forgery surface: an attacker who can spoof a reply-to-bot must not escalate to verb=mention. Sender authentication has to live at the channel boundary, before routing.
The loop
- Poll IMAP
INBOXfor messages newer than the last seen UID. - Parse
Authentication-Results. DMARC pass on the configured authserv-id (plus optionalFrom-domain allowlist) →verb=message. Anything else →verb=untrusted, optional[UNVERIFIED]subject prefix.EMAIL_STRICT_AUTH=1drops untrusted mail outright. - Look up the thread row by
Message-IDorReferences. Hit → reuse the existingchat_jid. Miss → mint a new one. - POST
/v1/messagesto routd, signed withCHANNEL_SECRET. - On outbound, routd calls back to
POST /send. emaid hands the body to SMTP withIn-Reply-To+Referencesset from the thread row.
How it fits
mail server (Gmail, Fastmail, …)
| IMAP IDLE / poll
v
emaid (parse Authentication-Results, classify verb)
| POST /v1/messages (signed by CHANNEL_SECRET)
v
routd
| POST /send (callback to emaid)
v
emaid (SMTP submit; In-Reply-To = thread root)
|
v
mail server → recipient
Inputs: IMAP traffic on the configured mailbox; HTTP callbacks from routd. Outputs: SMTP submissions; signed POSTs to routd. Hard deps: a reachable routd, an IMAP+SMTP account, and a trusted upstream MTA whose authserv-id is set in EMAIL_TRUSTED_AUTHSERV.
Sender authentication
Library: emersion/go-msgauth/authres. emaid does not run its own DMARC validator; it trusts the MTA that delivered the mail. The operator pins which MTA via EMAIL_TRUSTED_AUTHSERV (fail-closed default — unset value rejects every message). Three classification levers:
EMAIL_TRUSTED_AUTHSERV— required. authserv-id whose DMARC verdict to trust (e.g.mx.google.com).EMAIL_TRUSTED_DOMAINS— optional allowlist ofFrom-domains. When set, DMARC pass alone is not enough; the sender domain must also match.EMAIL_STRICT_AUTH— truthy (true/1/yes/on) drops untrusted mail with no agent turn. Default isfalse— untrusted mail still reaches the store withverb=untrusted.EMAIL_UNVERIFIED_SUBJECT_PREFIX— truthy adds an[UNVERIFIED]prefix to the subject when the message lands as untrusted.
The gateway will not promote an untrusted reply-to-bot to verb=mention (mention-promotion spec). Operators route verb=untrusted to a quarantine sub-group.
Standalone usage
emaid is a plain Go binary. It needs a reachable routd, an IMAP+SMTP account, and a writable directory for its thread store.
export ROUTER_URL=http://routd:8080
export CHANNEL_SECRET=$(grep ^CHANNEL_SECRET .env | cut -d= -f2)
export LISTEN_ADDR=:8080
export DATA_DIR=/srv/data/arizuko_demo
export EMAIL_IMAP_HOST=imap.fastmail.com EMAIL_IMAP_PORT=993
export EMAIL_SMTP_HOST=smtp.fastmail.com EMAIL_SMTP_PORT=587
export EMAIL_ACCOUNT=bot@example.com EMAIL_PASSWORD=...
export EMAIL_TRUSTED_AUTHSERV=mx.example.com
./emaid
GET /health returns 503 when IMAP login fails or the poll loop is erroring; 200 once a successful poll completes.
Verb support
Email has no native reactions, no edit, no delete. emaid implements send and fetch_history; every other Socializer verb returns chanlib.ErrUnsupported so the agent can pick a different action instead of silently dropping the call.
Go deeper
- components/channels — the shared adapter shape.
emaid/README.md— full env var table, verb support, file map.SECURITY.md— trust model; why untrusted senders cannot escalate.- concepts/routing — how to route
verb=untrustedto a quarantine sub-group.