← All documentation

Deliverability

Deliverability

Everything here is about being trusted by the mail servers you talk to. A gateway that scans perfectly but is refused at the door is worse than useless, so treat this as part of installation rather than as tuning.

1. Reverse DNS (PTR) on the gateway IP

This is the single highest-impact item. Many large receivers — including Microsoft 365 — reject mail outright from an IP with no PTR record, and most of the rest score it as suspicious.

PTR records live with whoever owns the IP address, so you set this in your hosting provider's control panel, not in your own DNS zone.

  1. Set the PTR for the gateway's public IP to your mail host, e.g. mx.example.com.
  2. Make sure mx.example.com has an A record pointing back to that same IP. Both directions must agree; this is "forward-confirmed reverse DNS", and some receivers check it explicitly.

Verify from the domain page in the dashboard (Reverse DNS panel), or:

dig +short -x 203.0.113.10      # should print mx.example.com.
dig +short mx.example.com       # should print 203.0.113.10

Also check your IP against the common blocklists before going live: https://multirbl.valli.org/. A residential or recently-recycled cloud IP is often already listed, and delisting takes days.

2. HELO name

The gateway announces itself with MAIL_HOST (inbound listener and platform outbound nodemailer EHLO via SMTP_EHLO_NAME or MAIL_HOST). It must be a fully-qualified name that resolves — not localhost, not a bare hostname. It should match the PTR record from step 1.

Egress workers announce EHLO with COLDFEET_EHLO_NAME in /etc/coldfeet-egress/config.env (falls back to the VPS public IP or hostname). Set each worker's EHLO to a name whose A/PTR matches that worker's public IP.

3. MTA-STS

SPF, DKIM and DMARC authenticate the sender. None of them stop an attacker on the network path from stripping the STARTTLS advertisement and reading mail in transit, because a sending MTA that cannot negotiate TLS silently falls back to cleartext. MTA-STS (RFC 8461) is what closes that: a sender that has fetched your policy refuses to deliver without a valid TLS connection.

It needs two things per domain:

RecordTypeValue
mta-sts.<domain>CNAMEyour mail host
_mta-sts.<domain>TXTv=STSv1; id=<policy id>

Both are shown, with the current policy id, on the domain page and in step 2 of the onboarding wizard.

The CNAME points the policy host at the gateway, which serves the policy at https://mta-sts.<domain>/.well-known/mta-sts.txt. nginx proxies that path to the API, which picks the right policy from the Host header and refuses to answer for domains it is not the MX for.

The certificate must cover the policy host. Senders validate it strictly and ignore the policy if it fails. After adding the CNAME, extend the certificate:

certbot certonly --nginx \
  -d mail.example.com \
  -d mta-sts.customer-one.com \
  -d mta-sts.customer-two.com
sudo systemctl reload nginx

A customer who would rather not CNAME to you can host the same policy file themselves; the body is shown on the domain page.

Modes

MTA_STS_MODE in .env.production controls what senders do on failure:

4. TLS reporting (TLS-RPT)

MTA-STS has no feedback channel of its own, so a broken certificate looks exactly like everything working. TLS-RPT (RFC 8460) gives senders somewhere to report failures:

RecordTypeValue
_smtp._tls.<domain>TXTv=TLSRPTv1; rua=mailto:tls-rpt@<mail host>

Reports arrive as mail to tls-rpt@<your mail host>, are parsed automatically, and appear on the domain page. Publish this before switching MTA-STS to enforce.

5. DMARC aggregate reports

The DMARC record generated during onboarding already points rua= at dmarc@<your mail host>. The gateway accepts mail for that address, unpacks the attachment (gzip, zip or bare XML) and stores each record.

One-time setup on your own zone. Because the reporting mailbox is on a different domain from the one being reported on, RFC 7489 requires the mailbox's domain to opt in. Reporters that enforce this — Google among them — will otherwise refuse to send anything. Publish a wildcard once, in the zone for your mail host:

*._report._dmarc.mail.example.com.  IN  TXT  "v=DMARC1"

Verify with dig +short TXT customer.com._report._dmarc.mail.example.com, which should return "v=DMARC1".

Reports show up on the domain page within a day or two, and answer the question you need before tightening policy: which senders are failing alignment? The domain page lists them explicitly. Work through that list, then move p=nonep=quarantinep=reject.

Reports can also be uploaded directly, which is useful for backfilling:

curl -X POST https://your-host/api/v1/tenants/$TENANT/domains/$DOMAIN/dmarc/reports \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d "{\"filename\":\"report.xml.gz\",\"content\":\"$(base64 -w0 report.xml.gz)\"}"

Re-uploading the same file updates the existing rows instead of double-counting.

6. Postmaster and abuse addresses

RFC 2142 requires postmaster@ and abuse@ to work for every domain you accept mail for. Some receivers check. Add them as recipients (or leave the domain in catch-all mode) and make sure someone reads them.

Also enrol the gateway IP in the feedback loops of the large mailbox providers, so complaints reach you rather than quietly damaging your reputation:

7. Egress workers (multi-IP send)

Coldfeet is an application SEG, not a Postfix/Amavis milter stack. The same stages still exist:

StageColdfeet
EdgeNest SMTP + DNSBL + allow/block lists
Content scanRspamd + ClamAV + AI/URL/DLP
AuthRspamd verify; app DKIM sign + ARC seal on leave
Body transformsStationery / banners / message crypto
DeliveryDovecot LMTP, domain relay, egress workers, or platform ESP relay

Workers only handle SMTP egress after ALLOW for traffic that needs an owned IP — inbound gateway delivery to the tenant smart host, and outbound direct MX. They do not accept inbound mail on port 25. The main Coldfeet IP is the sole inbound MX. Delivery via workers is direct_mx: the agent connects to the recipient MX (or tenant smart host) and “sent” means that hop accepted the message. For recipient MX delivery, the platform resolves an ordered MX list and the agent tries the next host on connect/4xx (5xx stops).

For outbound mail from a verified tenant domain, admin can add SES / SendGrid / Mailgun / Resend / Postmark / MailerSend relay accounts. Those sends leave from the platform (pooled authenticated SMTP); workers are not consumed. Domains can also terminate inbound at those providers (provider_mx); see RELAY-ACCOUNTS.md and INBOUND-PROVIDER-MX.md. MTA-STS is suppressed for provider-MX domains.

Verify inbound on the main IP (workers stay outbound)

After the API is healthy on the install host:

  1. docker compose psapi healthy (not restarting).
  2. ss -lntp | grep :25 — listener on 25 on the main host only.
  3. External: swaks --to user@customer.domain --server <MAIN_IP> -tlso (or openssl s_client -starttls smtp -connect MAIN_IP:25) for an onboarded domain.
  4. Egress node(s) ready; a direct_mx send completes (admin workers UI or agent logs).
  5. Confirm worker VPS hosts have no inbound SMTP bind (expected).

For each worker IP:

  1. Set PTR (and matching A) for the public IPv4.
  2. Set COLDFEET_EHLO_NAME in /etc/coldfeet-egress/config.env to that FQDN.
  3. Optionally list the IP on the customer's inbound connector — see below.

Platform outbound EHLO uses SMTP_EHLO_NAME or MAIL_HOST.

Worker IP warm-up and Microsoft throttles

New or rotated worker IPs start with no reputation. Microsoft (and others) often defer or throttle them until volume and complaint rates look normal. Treat warm-up as an ops step, not an afterthought:

  1. PTR before first send. Publish reverse DNS (and matching A) for every egress IPv4 before that IP sends production mail. Microsoft 365 will reject or heavily defer mail from IPs with no PTR.
  2. Enrol in SNDS and JMRP. Register every sending IP at Microsoft SNDS and enable JMRP so complaint feedback reaches you instead of silently burning reputation.
  3. Office 365 sender mitigation. If mail is deferred with 4.7.500 / Access denied, please try again later or similar after warm-up basics are in place, submit the Office 365 sender support / mitigation form for that IP or domain.
  4. Keep EHLO aligned with PTR (COLDFEET_EHLO_NAME on the worker). The platform job queue owns retries and destination throttles — there is no local Postfix queue on workers.
  5. ESP relay is outbound-only and skips workers. SES / SendGrid / Mailgun / Resend / Postmark / MailerSend only accept outbound mail whose From domain is verified in that account; inbound gateway traffic has an external From and is delivered via workers to the tenant smart host (or provider-MX webhook ingest), never via ESP selection keyed on the external From. Outbound ESP sends leave from the platform and do not consume per-node destination throttles.

Do I need an inbound connector on Microsoft 365?

No, not to deliver mail. tenant.mail.protection.outlook.com accepts mail from any IP for a domain that tenant owns; that is how every gateway in front of M365 works. A domain whose MX points at Coldfeet needs nothing configured on the Microsoft side for inbound mail to arrive.

An inbound connector is worth adding in two situations, both of them hardening rather than plumbing:

New-client throttling (450 … new clients, hourly caps) is a separate problem from connectors. Register worker IPs with Microsoft (SNDS / sender mitigation — see warm-up above); an inbound connector does not lift those limits.

Optional inbox-placement tuning once mail is flowing: enable Enhanced Filtering for Connectors (skip listing) on a partner connector, and add MAIL_HOST under Trusted ARC Sealers in Exchange Online so EOP can honour Coldfeet ARC seals when DKIM/SPF no longer align after rewrite.

If mail is deferring, the connector is almost never the cause. Check TLS verification on the delivery destination first (Coldfeet defaults it off for exactly this reason), then that the destination host is right.

Track which IPs have been listed under Admin → Workers → Connector IPs.

Outbound: sending through Coldfeet as a smart host

The direction that genuinely does need connector work is the other one — a customer's users sending outbound mail through Coldfeet so that DLP, stationery and DKIM signing apply. See SMART_HOST.md.

8. ARC for forwarding

When Coldfeet rewrites a message (banner, URL rewrite, stationery) or relays it to M365/external, the original sender’s DKIM often breaks and SPF no longer matches. Receivers then see DMARC fail even though the mail was legitimate when it arrived.

ARC (Authenticated Received Chain) lets Coldfeet attach a signed statement: “when I received this, SPF/DKIM/DMARC were X.” Later hops can trust that chain even if the original DKIM is broken.

Coldfeet:

  1. Verifies inbound ARC via Rspamd (ARC_ALLOW / ARC_REJECT).
  2. Seals outbound on the relay/egress leave path (after DKIM), using the same mailauth stack as DKIM. Local LMTP delivery is not sealed.
  3. Does not extend a chain that already failed (cv=fail).

Setup

  1. ARC signing keys auto-generate on API boot when MAIL_HOST is set and no key exists yet. You can also regenerate from Admin → Platform settings → ARC sealing.
  2. Publish the TXT record shown there (selector defaults to cfarc):
RecordTypeValue
cfarc._domainkey.<MAIL_HOST>TXTv=DKIM1; k=rsa; p=…
  1. Leave ARC_SEAL_ENABLED=true (default). Sealing logs an error (and skips) if no key is configured. Set ARC_SEAL_ENABLED=false to disable without deleting the key. Optional: ARC_SELECTOR=cfarc.
  2. Optionally, on Microsoft 365, add MAIL_HOST under Trusted ARC Sealers once mail is flowing (inbox-placement tuning — see inbound connector section above).

Workers send the already-sealed EML unchanged — no agent change.

Checklist before going live