Skip to main content

Inbound Email Bridge

AuthProxy includes a built-in SMTP listener on port 25 that turns inbound email into chat messages. The same code@<project-domain> recipient shape works for two flavours of conversations:

Local-part codeDecoded typeBehaviour
Anonymous chat inviteChatInvite (339) / GroupChatThe message is written into the invite's anonymous chat from a deterministic anon visitor identity.
CRM userCrmUser (209)The sender's email must be registered in user_exid. AuthProxy creates (or reuses) a private chat between the sender and recipient CRM users and posts the message from the sender's side.
Anything elseReject with 550 5.1.1 Unknown recipient.

Reply-from-chat back to email is implemented as a fire-and-forget hook in the Chat module (Services/EmailNotifyHook.cs); it runs automatically for chats that have at least one message with ChatMessFlags.Email.

Verification surface

A live deployment exposes the following observable signals:

  • AuthProxy startup log: SMTP inbound listener started on :25 (TLS required after EHLO).
  • Plain socket smoke check: telnet <vm-ip> 25 should return 220 <project-domain> ESMTP ItBuild.
  • Functional test with swaks:
swaks --to abc123code@example.com \
--from sender@gmail.com \
--server <vm-ip>:25 \
--tls --tls-optional \
--header "Subject: hi"
  • Uptrace activity smtp.inbound.session carries the per-session tags smtp.helo, smtp.from, smtp.rcpt, smtp.code, smtp.dkim_domain, smtp.auth_verdict, smtp.auth_reason, smtp.status, smtp.error_code, smtp.size_bytes, smtp.chat_id, smtp.message_id.

Authentication

Inbound mail is authenticated end-to-end before the body is forwarded to Chat:

  • DKIM — RFC 6376 rsa-sha256 with relaxed/relaxed canonicalization. simple and ed25519-sha256 are not supported in V1 and are returned as dkim=unsupported.
  • SPF — minimal RFC 7208 evaluator. Supported mechanisms: ip4, ip6, include, redirect, all. a, mx, ptr, exists, and macros are treated as permerror → fail. Missing SPF record (SPF none) is treated as fail.
  • From-alignment — strict: the RFC 5322 From: domain must match the DKIM d= and the SPF-tested envelope sender. There is no DMARC relax policy in V1.

The EmailAuthVerifier packs verdict + reason into Uptrace span tags so failures can be diagnosed without enabling debug logs.

Configuration (appsettings.json)

Only the values that genuinely vary per deployment live in the config; project domain, attachment size limit, SMTP port, and recipients-per-session limit are baked into the code (see source-of-truth section below).

"InboundEmail": {
// Master switch. Flip to true after the customer DNS / NAT / TLS setup is complete.
"Enabled": false,

// Per-process upper bound on simultaneous SMTP sessions; tune for VM resources.
"MaxConcurrentConnections": 50,

// Idle timeouts (seconds).
"CommandTimeoutSec": 60,
"DataTimeoutSec": 300,

// Public DNS resolvers. Predictable behaviour across deploys.
"DnsServers": [ "8.8.8.8", "1.1.1.1" ],
"DnsTimeoutMs": 2000,

// Hourly caps per sender / per peer IP. 0 disables.
"RateLimitPerSenderHour": 30,
"RateLimitPerIpHour": 60,

// PFX path for STARTTLS. Production deployments MUST set both fields; without a cert the
// listener refuses to start outside Test / Local / LocalDocker / Development.
"TlsCertPath": null,
"TlsCertPassword": null
}

Source-of-truth values (not in config)

ValueSource
Project domainItBuild.Shared.License.Domain (build-time injected).
Maximum attachment sizeDynamic FileMaxSizeBytes setting (default 20 MB).
SMTP port25.
Max recipients per session1.

Customer-side setup checklist

  1. MX record for the project domain:

    @     IN MX 10 mail.<License.Domain>
    mail IN A <VM-public-IP>
  2. Edge gateway / firewall rule: public SMTP port → project VM SMTP port (TCP).

  3. Reverse DNS / PTR for the VM public IP must point to mail.<License.Domain>. Without rDNS, most large mail providers (Gmail, Microsoft, Yahoo) classify our outbound notifications as spam. Request rDNS from your hosting provider.

  4. SPF TXT for <License.Domain> (notification credibility): v=spf1 ip4:<VM-IP> -all.

  5. DKIM TXT (optional, V2 outbound signing): default._domainkey IN TXT "v=DKIM1; k=rsa; p=<base64>".

  6. DMARC TXT (optional): _dmarc IN TXT "v=DMARC1; p=none".

DNS records on your domain are about you as a sender. Inbound DKIM/SPF authentication queries the sender's DNS, so the inbound flow does not require any DNS records on your domain.

Common reject codes

CodeMeaningWhere it is emitted
421 4.7.0 Rate limitPer-sender or per-IP hourly cap exceededRateLimiter
501 5.5.0 Invalid HELOHELO/EHLO arg isn't an FQDN or [IP-literal]Pre-MAIL FROM
530 5.7.0 Must issue STARTTLS firstTLS required but client skipped STARTTLSTLS-required mode
550 5.1.1 Unknown recipientDomain isn't ours, code doesn't decode, or CRM user not foundRCPT TO / Chat dispatch
550 5.7.1 Sender auth failedDKIM / SPF / alignment didn't passAfter DATA
552 5.3.4 Message too largeBody exceeded FileMaxSizeBytes * 4/3 + 64KBDATA reader
554 5.5.0 Bad end-of-data sequenceSMTP smuggling vector detected (\n.\n, \r.\r\n)DATA reader

Troubleshooting

  • Mail accepted but invisible in chat — check Chat module logs for inbound_email_send and the message's flags. The message probably has ChatMessFlags.PendingUpload because the attachment upload failed; AuthProxy then issues delete_message to clean up.
  • Auth always fails for a known good sender — check smtp.auth_reason in Uptrace. Common causes: the signer uses c=simple/relaxed (V1 supports relaxed/relaxed only), key length mismatch, or the From: domain doesn't match the DKIM d= (alignment failure).
  • Notifications not arriving back — the Chat module uses the same SMTP relay configured under Config.SmtpMail. Verify SmtpHost / SmtpPort and that the outbound IP is allowed by the relay. Enable MailSender = SmtpSender.

Out of scope for V1

  • Multiple attachments per email (90% case is one attachment).
  • DMARC alignment policies (V1 enforces strict From:-alignment unconditionally).
  • DKIM ed25519-sha256, simple canonicalization (returned as dkim=unsupported).
  • HTML → markdown conversion (V1 prefers text/plain; HTML-only payloads land in chat as a placeholder).
  • Quote stripping in replies, full RFC 2231 split-parameter decoding.
  • Per-recipient debounce / retry on outbound failure (V1 fires once per message).
  • MTA-STS, TLS-RPT, DANE.