SMTP Submission
Connect any app or service that speaks SMTP.
Replace example addresses with your enrolled sending address and a real recipient address you control. The relay rejects IANA example domains such as example.com as recipients.
Connection details
Section titled “Connection details”| Host | smtp.atmos.email |
| Port | 587 (STARTTLS) |
| Security | STARTTLS required |
| Username | Your DID, or any sending domain enrolled under it (e.g. yourdomain.com) |
| Password | Your API key (atmos_…) |
The key must be allowed to use the sending domain. See creating and limiting API keys.
The username is not your atproto handle. The relay accepts your DID
(did:plc:...) or, as a more SMTP-friendly alternative, any sending domain
enrolled under that DID. Most clients use the domain because the : in a DID
breaks URL-style auth configs (nodemailer, PDS env vars, etc.).
How it works
Section titled “How it works”- Your app connects to
smtp.atmos.email:587and upgrades to TLS via STARTTLS. - Your app authenticates with SASL PLAIN using your DID or sending domain as the username, and your API key as the password.
- Your app sends the message using standard SMTP commands (MAIL FROM, RCPT TO, DATA).
- The relay applies dual DKIM signatures, checks warming limits, and delivers to the recipient’s MX.
Configuration examples
Section titled “Configuration examples”Nodemailer (Node.js)
Section titled “Nodemailer (Node.js)”const transporter = nodemailer.createTransport({ host: "smtp.atmos.email", port: 587, secure: false, // STARTTLS auth: { user: "yourdomain.com", pass: "atmos_your-api-key", },});Python (smtplib)
Section titled “Python (smtplib)”import smtplibfrom email.message import EmailMessage
msg = EmailMessage()msg["From"] = "you@yourdomain.com"msg["To"] = "friend@example.com"msg["Subject"] = "Hello"msg.set_content("Sent via comail")
with smtplib.SMTP("smtp.atmos.email", 587) as server: server.starttls() server.login("yourdomain.com", "atmos_your-api-key") server.send_message(msg)Go (net/smtp)
Section titled “Go (net/smtp)”auth := smtp.PlainAuth("", "yourdomain.com", "atmos_your-api-key", "smtp.atmos.email")err := smtp.SendMail("smtp.atmos.email:587", auth, "you@yourdomain.com", []string{"friend@example.com"}, []byte(msg))PDS configuration
Section titled “PDS configuration”If you run a self-hosted atproto PDS that sends email (verification, notifications), point its SMTP config at the relay:
PDS_EMAIL_SMTP_URL=smtp://yourdomain.com:atmos_your-api-key@smtp.atmos.email:587The relay handles DKIM signing. No additional DNS records beyond the ones from enrollment.
- The
MAIL FROMdomain must match a domain enrolled under the authenticated DID. See Identity and sending domains. - Port 465 (implicit TLS) is unavailable. Use port 587 with STARTTLS.
- Messages hit warming tier rate limits during the first 14 days.
- The relay adds
List-UnsubscribeandList-Unsubscribe-Postheaders to explicitly taggedbulkorbroadcastmail per RFC 8058. One-click unsubscribes add the recipient to your suppression list. If you already include your ownList-UnsubscribeorList-Unsubscribe-Postheader in the message, comail detects it and does not inject its own. You own the pair and are responsible for a valid RFC 8058 combination:List-Unsubscribemust contain an HTTPS URI (amailto:URI may additionally be present) andList-Unsubscribe-Postmust be exactlyList-Unsubscribe=One-Click. Suppression-list enforcement stays active either way. See Categories for details. - Prefer the HTTP API from serverless environments (Cloudflare Workers, Vercel Edge) where raw TCP sockets are unavailable.
- Accepted messages (
250 OK) are fsynced before the response. The relay retries failed deliveries. See Reliability for the retry schedule and status vocabulary. - See Troubleshooting for common errors.