Mail transport
Protege is email-native, but it adds no transport of its own — both directions are plain Rails configuration:
- Inbound is Action Mailbox: you pick an ingress, and the engine registers the persona routing.
- Outbound is Action Mailer SMTP: the
send_emailtool hands the reply to the Gateway, which delivers through your SMTP settings.
In development you need no real mail server — use the Action Mailbox conductor. This page covers production, where there are two paths. Start with a provider — it's config-only and works on any host. Self-hosting an MTA is the advanced path; reach for it only when you specifically want no third party in the loop.
Using an email provider
A transactional email provider gives you both directions with no servers to run: an inbound webhook Action Mailbox understands natively, and SMTP for outbound. Rails ships ingresses for Mailgun, Mandrill, Postmark, and SendGrid, plus a generic :relay. The example uses Postmark; any provider with an Action Mailbox ingress works the same way.
# config/environments/production.rb
# Inbound — the provider POSTs received mail to Action Mailbox with HTTP Basic auth.
config.action_mailbox.ingress = :postmark
config.action_mailbox.ingress_password = ENV["RAILS_INBOUND_EMAIL_PASSWORD"]
# Outbound — deliver through the provider's SMTP endpoint.
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: "smtp.postmarkapp.com",
port: 587,
user_name: ENV["POSTMARK_API_TOKEN"],
password: ENV["POSTMARK_API_TOKEN"],
authentication: :plain,
enable_starttls_auto: true
}Then, in the provider's dashboard:
- Verify
thescoop.comand add the DNS records it gives you (typically SPF + DKIM, often a custom Return-Path/CNAME). The provider signs and authenticates on your behalf. - Point inbound at your app: set the provider's inbound webhook to
https://your-host/rails/action_mailbox/postmark/inbound_emailswith a password matchingRAILS_INBOUND_EMAIL_PASSWORD. For an MX-based inbound provider, set the domain's MX record to the provider's mail host.
That's the whole round-trip — no port 25, no PTR, no MTA.
Sender approval
Some providers gate new accounts to "send only to verified addresses" until approved. If your first outbound test is rejected by a sender policy, that's account approval, not your configuration.
Self-hosting with Postfix + OpenDKIM
If you want no provider at all, run your own MTA. Protege ships a reference Postfix + OpenDKIM image you run as a Kamal accessory: it receives mail on port 25, pipes each message to the generic :relay ingress, and OpenDKIM-signs outbound. One image serves every agent domain — the app pushes each domain's DKIM key to the running MTA when you add it, so nothing is baked in.
Self-hosting means owning deliverability
Many hosts block outbound port 25 by default (you must request an unblock, and they may decline). Inbox placement depends on reverse DNS (PTR), correct SPF/DKIM/DMARC, and a clean IP reputation a fresh address won't have — expect early mail to land in spam. Prefer a provider unless you specifically want no third party in the path.
1. Scaffold the mail server
bin/rails g protege:postfixThis writes the container build into deploy/mail/ (Dockerfile, Postfix/OpenDKIM/OpenDMARC config, the provisioning listener, and a full MAIL.md deploy guide), a Kamal accessory example, and a GitHub Actions workflow to build the image.
2. Point the app at the local MTA
# config/environments/production.rb
# Inbound — Postfix pipes each message to the generic relay ingress.
config.action_mailbox.ingress = :relay
config.action_mailbox.ingress_password = ENV["RAILS_INBOUND_EMAIL_PASSWORD"]
# Outbound — deliver to the mail accessory over the internal network (same-host hop; no auth, no TLS).
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: "protege-mail", # the accessory's name on the Kamal network
port: 25,
authentication: nil,
enable_starttls_auto: false
}Set MAIL_RELAY_HOST (the accessory name, e.g. protege-mail) in the app service's env too — the app pushes provisioning there, and its presence puts the console in self-hosted mode.
3. Add each agent domain in the dashboard
Each EmailDomain record generates an RSA keypair and the exact DNS records to publish. Add a domain under Domains → New and Protege pushes its DKIM key to the MTA automatically — no secret to copy, no reboot. Use Sync now on a domain page to re-push after an accessory reboot.
4. Publish DNS, rDNS, and open the firewall
Read the record values straight off the domain record:
| Record | Location | Example value |
|---|---|---|
MX | the domain apex | 10 thescoop.com. |
TXT (SPF) | the domain apex | v=spf1 mx ~all |
TXT (DKIM) | default._domainkey.thescoop.com | v=DKIM1; k=rsa; p=… |
TXT (DMARC) | _dmarc.thescoop.com | v=DMARC1; p=reject; adkim=s; aspf=s; … |
Then set the server IP's PTR / reverse DNS to the MTA's hostname, allow inbound TCP 25 so the internet can reach your MX, and confirm your host permits outbound 25 (without it you can receive but not send).
5. Inbound authentication enforcement
The bundled MTA authenticates incoming mail at SMTP — policyd-spf checks SPF, OpenDKIM verifies DKIM, and OpenDMARC rejects mail that fails DMARC against a p=reject sender. This is what keeps a spoofed From: from satisfying the engine's access guardrail. The accessory's DMARC_POLICY env var controls it: enforce (default) rejects failures; monitor evaluates and stamps Authentication-Results headers but rejects nothing — deploy with monitor first for an observation window, then switch to enforce (no rebuild).
6. Verify
Deploy and watch the MTA (kamal accessory logs mail). Email an agent from an outside account and confirm a reply is composed in /protege (inbound works as soon as MX + inbound 25 are set); the reply should arrive (needs outbound 25), and in Gmail Show original should read SPF=pass, DKIM=pass, DMARC=pass. Score deliverability at mail-tester.com and iterate on rDNS and reputation.
The generated deploy/mail/MAIL.md walks through all of this in more detail.
Related
- Generators & CLI — the
protege:postfixgenerator. - Security — why inbound authentication matters to the access guardrail.
- Models — the
EmailDomainmodel and its DNS helpers.

