architecture
SMTP Pipeline
How inbound emails are received, parsed, and stored.
Overview
The SMTP server (cmd/smtpd) is a custom implementation that handles inbound email delivery. It listens for SMTP connections, validates recipients against known domains and inboxes, parses MIME content, and stores emails with their attachments.
Connection Flow
- TCP Accept — Listener accepts the connection; at most 100 concurrent, beyond which the connection is closed immediately
- EHLO/HELO — Server advertises
SIZE,8BITMIME,PIPELINING,ENHANCEDSTATUSCODES, andSTARTTLSwhen bothsmtp.tls_certandsmtp.tls_keyare set - MAIL FROM — Sender address recorded
- RCPT TO — Recipient validated against the domain router; at most 100 recipients per message (
452beyond that) - DATA — Message read under a 5-minute deadline, rejected with
552if it exceedssmtp.max_size(default 25 MB) - Parse and enqueue — The listener parses the MIME envelope with enmime and pushes one message per recipient onto the queue (
smtp.queue_size, default 1000); a full queue answers451 - Processing — A pool of
smtp.workersgoroutines (default 4) stores each queued message and runs the fan-out
Parsing happens in the listener, before the queue — the worker pool receives an
already-parsed message. A message enmime cannot parse at all is still stored,
with the raw bytes as the text body and the subject (parse error).
Domain Router
The router checks each recipient address against:
- Domain lookup — Is the domain registered in the database? (Verification is enforced earlier — when the domain is assigned to a team, and again when an inbox is created — not at delivery time.)
- Redis inbox lookup — Is there an active inbox for this address?
- PostgreSQL fallback — If not in Redis, check the database directly
If no matching inbox is found, the email is rejected with a 550 error.
MIME Parsing
Emails are parsed using the enmime library which handles multipart messages (text/plain + text/html), nested MIME parts, inline and attached files, character encoding detection and conversion, and malformed email recovery.
Attachment Handling
Attachments are processed through the settings resolver which checks the inheritance cascade:
- Is the extension on the executable blocklist? Sixteen are refused outright, before any size check:
.exe,.bat,.cmd,.com,.msi,.scr,.pif,.vbs,.js,.wsh,.wsf,.ps1,.hta,.cpl,.reg,.inf - Are attachments enabled at the org level?
- Are attachments enabled for this domain or domain assignment?
- Does the file exceed the resolved maximum size?
A rejected attachment is skipped and logged; the rest of the message is still
delivered. If every attachment is skipped, the optimistic has_attachments flag
is corrected back to false.
If allowed, attachments are uploaded to object storage (MinIO/S3, or the local-filesystem fallback) under attachments/{email_id}/{random_uuid}/{sanitized_filename}, and the key is recorded in attachments.storage_key.
Fan-out
Before the row is written, the worker computes a spam score (0–10, from missing
Message-ID / Subject / Date / From headers, a Received-SPF fail, and a
live SPF lookup against the connecting IP) and sanitizes the HTML body with
bluemonday's UGCPolicy. Only the sanitized HTML is persisted.
After storage, the handler triggers:
- Realtime — Publishes an inbox event on the
bb:inboxRedis channel; the API server's bridge broadcasts it to the inbox hub, pushes it to the owner's notification hub, persists anotificationsrow, and increments the analytics counters - Webhooks — Dispatches
email.receivedto every active webhook on the owning team subscribed to that event - Audit — Records an
email.receivedentry against the inbox's org, with the inbox address as the resource name