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 code | Decoded type | Behaviour |
|---|---|---|
| Anonymous chat invite | ChatInvite (339) / GroupChat | The message is written into the invite's anonymous chat from a deterministic anon visitor identity. |
| CRM user | CrmUser (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 else | — | Reject 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> 25should return220 <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.sessioncarries the per-session tagssmtp.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-sha256withrelaxed/relaxedcanonicalization.simpleanded25519-sha256are not supported in V1 and are returned asdkim=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 DKIMd=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)
| Value | Source |
|---|---|
| Project domain | ItBuild.Shared.License.Domain (build-time injected). |
| Maximum attachment size | Dynamic FileMaxSizeBytes setting (default 20 MB). |
| SMTP port | 25. |
| Max recipients per session | 1. |
Customer-side setup checklist
-
MX record for the project domain:
@ IN MX 10 mail.<License.Domain>
mail IN A <VM-public-IP> -
Edge gateway / firewall rule: public SMTP port → project VM SMTP port (TCP).
-
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. -
SPF TXT for
<License.Domain>(notification credibility):v=spf1 ip4:<VM-IP> -all. -
DKIM TXT (optional, V2 outbound signing):
default._domainkey IN TXT "v=DKIM1; k=rsa; p=<base64>". -
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
| Code | Meaning | Where it is emitted |
|---|---|---|
421 4.7.0 Rate limit | Per-sender or per-IP hourly cap exceeded | RateLimiter |
501 5.5.0 Invalid HELO | HELO/EHLO arg isn't an FQDN or [IP-literal] | Pre-MAIL FROM |
530 5.7.0 Must issue STARTTLS first | TLS required but client skipped STARTTLS | TLS-required mode |
550 5.1.1 Unknown recipient | Domain isn't ours, code doesn't decode, or CRM user not found | RCPT TO / Chat dispatch |
550 5.7.1 Sender auth failed | DKIM / SPF / alignment didn't pass | After DATA |
552 5.3.4 Message too large | Body exceeded FileMaxSizeBytes * 4/3 + 64KB | DATA reader |
554 5.5.0 Bad end-of-data sequence | SMTP smuggling vector detected (\n.\n, \r.\r\n) | DATA reader |
Troubleshooting
- Mail accepted but invisible in chat — check Chat module logs for
inbound_email_sendand the message'sflags. The message probably hasChatMessFlags.PendingUploadbecause the attachment upload failed; AuthProxy then issuesdelete_messageto clean up. - Auth always fails for a known good sender — check
smtp.auth_reasonin Uptrace. Common causes: the signer usesc=simple/relaxed(V1 supportsrelaxed/relaxedonly), key length mismatch, or theFrom:domain doesn't match the DKIMd=(alignment failure). - Notifications not arriving back — the Chat module uses the same SMTP relay configured under
Config.SmtpMail. VerifySmtpHost/SmtpPortand that the outbound IP is allowed by the relay. EnableMailSender = 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,simplecanonicalization (returned asdkim=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.
Related
- Authentication API
- Webhooks — outbound delivery model
- User scopes