docs
Operational
Documentation
Rail is a transactional email relay with mTLS authentication, built-in DNS, DKIM signing, and webhook-based inbound delivery.
Overview
# Outbound (your app → recipient)
Your App :465 (mTLS) Rail MX delivery + DKIM signing
# Inbound (recipient → your app)
Internet MTA :25 (STARTTLS) Rail Webhook POST to your app
465 SMTP submission (mTLS, client cert required)
25 Inbound SMTP (STARTTLS, receives mail from the internet)
80 HTTP (ACME challenges, redirect to HTTPS)
443 HTTPS (management API, cert inspection, stats)
53 DNS (authoritative: MX, SPF, DKIM, DMARC)
Client Certificates
# Issue a client cert with sender addresses and webhook URL
rail certs issue \
  --config /etc/rail/rail.yaml \
  --name myapp \
  --email noreply@ataca.io \
  --email alerts@smtp.ataca.io \
  --webhook https://myapp.com/hooks/email
CN Client name (e.g. "myapp") — used for audit logging
Email SANs Allowed sender addresses — MAIL FROM must match one of these
URI SANs Webhook URL (https://) — where inbound mail is delivered
# The From address must match an email SAN in the client cert.
# Local part is case-sensitive, domain is case-insensitive (RFC 5321).
# Cert has: noreply@ataca.io, alerts@smtp.ataca.io
MAIL FROM:<noreply@ataca.io> # ✓ allowed
MAIL FROM:<alerts@smtp.ataca.io> # ✓ allowed
MAIL FROM:<other@ataca.io> # ✗ 550 sender not authorized
API Reference
curl --cert client.crt --key client.key \
  -X POST https://smtp.ataca.io/api/v1/send \
  -H "Content-Type: application/json" \
  -d '{
    "request_id": "unique-id-123",
    "from": "noreply@ataca.io",
    "to": ["user@example.com"],
    "subject": "Your order shipped",
    "body_text": "Your package is on the way.",
    "unsubscribe_url": "https://ataca.io/unsubscribe?id=123"
  }'
# Response (202 Accepted):
{"id": "01KMT...", "status": "queued", "recipients": 1}
Optional fields: reply_to (override VERP reply address), body_html (HTML body part; when omitted, an HTML alternative is auto-generated from body_text), cc (CC recipients), unsubscribe_url (injects List-Unsubscribe and List-Unsubscribe-Post: List-Unsubscribe=One-Click per RFC 8058; DKIM-signed).
curl --cert client.crt --key client.key \
  https://smtp.ataca.io/api/who
# Response:
{"client_id": "myapp", "senders": ["noreply@ataca.io"],
 "valid": true, "key_type": "ed25519", ...}
curl -X POST --data-binary @client.crt \
  https://smtp.ataca.io/api/who
# HTML form posts (urlencoded/multipart) become email to the form target's
# recipients. Created with `rail forms create`; special fields _replyto,
# _subject, _redirect, _gotcha (honeypot). JSON with Accept: application/json.
# With forms captcha configured, spam-likely posts get a Turnstile challenge
# (HTML interstitial, or 403 challenge_required for JSON callers).
curl -d 'name=Alice&message=hi' https://smtp.ataca.io/f/01jx…
# Create (from must match a cert email SAN) → 201 with the public token.
# "cc_submitter": true CC's the submitter's email/_replyto address a copy.
# "field_labels": {"first_name":"First Name"} sets display labels for the
# rendered email; unlisted fields auto-prettify (first_name → First Name).
curl --cert client.crt --key client.key -X POST https://smtp.ataca.io/api/v1/forms \
  -H 'Content-Type: application/json' -d '{"name":"contact","from":"noreply@…","to":["you@…"],"subject":"Contact"}'
# PATCH updates one target: send only the fields to change (absent = keep,
# ""/[]/{} = clear); from/token/client_id are immutable and ignored if sent.
curl --cert client.crt --key client.key -X PATCH https://smtp.ataca.io/api/v1/forms/01jx… \
  -H 'Content-Type: application/json' -d '{"rate_limit":60,"enabled":false}'
# Every ACCEPTED submission is stored (honeypot/challenged/rate-limited/
# all-suppressed ones never are). Newest-first; ?before=<id> keyset-pages,
# ?limit= default 50/max 100. Owner-scoped like the form target — a
# token or id you don't own answers 404.
curl --cert client.crt --key client.key https://smtp.ataca.io/api/v1/forms/01jx…/submissions
curl https://smtp.ataca.io/healthz # 200 "ok"
curl https://smtp.ataca.io/readyz # 200 or 503 (draining)
curl https://smtp.ataca.io/stats # JSON: uptime, delivered, rate, latency
# Opt-in browser dashboard: password login (admin or client CN),
# stats overview + per-client message/delivery views.
Inbound Email
# Internet MTA connects to port 25 with STARTTLS
# RCPT TO must be a local domain — non-local is rejected (no open relay)
# Message is stored and POSTed to the client's webhook URL
# Local domains are configured in rail.yaml:
local_domains: [ataca.io, smtp.ataca.io]
# Additional domains auto-discovered from client cert email SANs
# Webhook URL from client cert URI SAN (set at cert issuance)
# POST to your webhook URL with JSON body:
{
  "message_id": "01KMT1GZ...",
  "from": "sender@gmail.com",
  "to": ["noreply@ataca.io"],
  "subject": "Re: Your order shipped",
  "raw_message": "<base64-encoded RFC 5322 message>",
  "received_at": "2026-03-28T12:00:00Z"
}
# 2xx = delivered, 4xx = permanent failure, 5xx = retry with backoff
# When a message you sent hard-bounces, rail POSTs a bounce notification.
# message_id is the ORIGINAL message; no raw_message. Soft bounces send nothing.
{
  "type": "bounce",
  "message_id": "01KMT1GZ...",
  "received_at": "2026-03-28T12:00:00Z",
  "original_message_id": "01jw...",
  "bounced_recipient": "user@example.com",
  "bounce_status": "5.1.1",
  "bounce_diagnostic": "smtp; 550 5.1.1 User unknown",
  "bounce_type": "hard"
}
# A verified spam complaint (ARF report MAC-verified against the message's VERP
# tags) POSTs a complaint notification and auto-suppresses the recipient.
# message_id is a unique event id; no raw_message.
{
  "type": "complaint",
  "message_id": "01KMT1GZ...",
  "received_at": "2026-03-28T12:00:00Z",
  "original_message_id": "01jw...",
  "complained_recipient": "user@example.com",
  "feedback_type": "abuse",
  "complaint_source": "arf"
}
Sending Examples
Send email via SMTP or the HTTP API. Both require a client certificate (mTLS).
Go
import (
  "crypto/tls"
  "net/smtp"
)
func sendMail() error {
  cert, _ := tls.LoadX509KeyPair("client.crt", "client.key")
  conn, _ := tls.Dial("tcp", "smtp.ataca.io:465", &tls.Config{
    Certificates: []tls.Certificate{cert},
  })
  c, _ := smtp.NewClient(conn, "smtp.ataca.io")
  c.Mail("noreply@ataca.io")
  c.Rcpt("user@example.com")
  w, _ := c.Data()
  w.Write([]byte("Subject: Hello\r\n\r\nHi!"))
  w.Close()
  return c.Quit()
}
cert, _ := tls.LoadX509KeyPair("client.crt", "client.key")
client := &http.Client{Transport: &http.Transport{
  TLSClientConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
}}
body := `{"from":"noreply@ataca.io",` +
  `"to":["user@example.com"],` +
  `"subject":"Hello","body_text":"Hi!"}`
resp, _ := client.Post(
  "https://smtp.ataca.io/v1/send",
  "application/json",
  strings.NewReader(body),
)
Python
import smtplib, ssl
ctx = ssl.create_default_context()
ctx.load_cert_chain("client.crt", "client.key")
with smtplib.SMTP_SSL("smtp.ataca.io", 465, context=ctx) as s:
  s.sendmail(
    "noreply@ataca.io",
    ["user@example.com"],
    "Subject: Hello\r\n\r\nHi!",
  )
import requests
resp = requests.post(
  "https://smtp.ataca.io/v1/send",
  cert=("client.crt", "client.key"),
  json={
    "from": "noreply@ataca.io",
    "to": ["user@example.com"],
    "subject": "Hello",
    "body_text": "Hi!",
  },
)
Certificate Inspection
# Identify yourself: rail inspects the client cert you present
curl --cert client.crt --key client.key \
  https://smtp.ataca.io/api/who
# Upload a .crt or .pem file; no client cert required.
# Anonymous responses omit the valid/revoked verdicts (mTLS shows them).
curl -X POST --data-binary @client.crt \
  https://smtp.ataca.io/api/who
Rust
use lettre::{Transport, SmtpTransport, Message};
use lettre::transport::smtp::client::TlsParameters;
let email = Message::builder()
  .from("noreply@ataca.io".parse()?)
  .to("user@example.com".parse()?)
  .subject("Hello")
  .body("Hi!".to_string())?;
// Load client cert for mTLS
let tls = TlsParameters::builder("smtp.ataca.io".into())
  .add_root_certificate(ca_cert)
  .identity(client_identity)
  .build()?;
let mailer = SmtpTransport::relay("smtp.ataca.io")?
  .port(465)
  .tls(lettre::transport::smtp::client::Tls::Wrapper(tls))
  .build();
mailer.send(&email)?;
let client = reqwest::Client::builder()
  .identity(identity) // mTLS client cert
  .build()?;
let resp = client.post("https://smtp.ataca.io/v1/send")
  .json(&serde_json::json!({
    "from": "noreply@ataca.io",
    "to": ["user@example.com"],
    "subject": "Hello",
    "body_text": "Hi!",
  }))
  .send().await?;