Migrate from Postal
CamelMailer began as a ground-up Rust rewrite of Postal and kept its protocol behaviour on purpose, so this is the shortest migration of the three. Your postal.yml loads unchanged, the API authentication header is the same, and organizations, servers, domains, credentials, routes, and webhooks all carry across.
Migrate automatically
camelmailer-migrate reads your Postal database and recreates its configuration through the CamelMailer admin API in one command: organizations, servers, domains, credentials, webhooks, routes, and IP pools. It carries over the two things that usually make a mail migration painful, so nothing on your side has to change:
- DKIM keys.Each domain's existing private key is imported, so the DKIM signature and your sending reputation stay the same. On a self-hosted target the DNS record needs no change at all.
- API and SMTP credential keys. The key values are preserved, so anything already sending through Postal keeps sending through CamelMailer with no code change.
The target URL alone decides where it writes. A *.camelmailer.com host is the hosted cloud (a user token, into one organization you name with --org); any other host is a self-hosted install (the machine X-Admin-API-Key, which can create organizations and force-verify domains). Install it with Cargo, or download a binary from the releases:
cargo install --git https://github.com/camelmailer/camelmailer-migrate
Always start with --dry-run. It reads Postal and prints exactly what it would create, and writes nothing:
camelmailer-migrate \ --postal-db mysql://postal:password@127.0.0.1:3306/postal \ --target https://app.camelmailer.com \ --api-key "$CAMELMAILER_API_KEY" \ --org acme \ --dry-run
For a self-hosted target, use the admin key. Omit --orgto mirror Postal's own organizations, or pass one to put everything under a single organization:
camelmailer-migrate \ --postal-db mysql://postal:password@127.0.0.1:3306/postal \ --target https://mail.example.com \ --api-key "$CAMELMAILER_ADMIN_API_KEY"
Multi-organization Postal installs come across whole. Without --org, every Postal organization is recreated, and each server, with its domains, credentials, webhooks and routes, lands under the organization it belonged to. The hosted cloud always migrates into the one organization you name, so several Postal organizations merge into it there.
By default only configuration comes across. Add --history to also migrate each server's past messages, with their delivery attempts, opens and clicks, read from Postal's separate per-server message databases. They are written as completed records through a non-sending import, so nothing is ever re-delivered. Choose how much of each message you keep with --history-bodies: full (headers and body), headers (headers only), or index (synthesized headers, no body). You can also leave config categories out with --skip (for example --skip webhooks,routes).
camelmailer-migrate \ --postal-db mysql://postal:password@127.0.0.1:3306/postal \ --target https://mail.example.com \ --api-key "$CAMELMAILER_ADMIN_API_KEY" \ --history --history-bodies full
What maps to what
Almost every Postal concept has a direct counterpart in CamelMailer. The biggest change is the API surface, which consolidates Postal's v1 and Admin APIs into one versioned v2.
| Postal | CamelMailer |
|---|---|
Server API key (X-Server-API-Key) | Server API credential, same X-Server-API-Key header. Create it under Credentials. |
| Domains with per-domain DKIM and an SPF include | Sending domains with a per-domain RSA-2048 DKIM key, an SPF include, and a verification TXT record. |
POST /api/v1/send/message | POST /api/v2/server/messages |
Send fields from, to, subject, html_body, plain_body | Same fields, with plain_body renamed to text_body. |
| Suppression list per server | Suppressions per server, honored before every send. |
| Webhooks (message events) | Webhooks, RSA-signed, for the outgoing delivery lifecycle. |
| Message routes (inbound) | Routes, same idea, matched at RCPT TO. |
| Ruby on Rails + MariaDB, one DB per mail server | One Rust binary + one PostgreSQL, with row-level security per tenant |
Your postal.yml loads unchanged
CamelMailer reads an existing postal.yml as-is: the postal: config group is accepted as an alias for camelmailer:, and POSTAL_CONFIG_FILE_PATH still points the binary at your file. Point the new process at the same configuration and it starts with your existing settings.
--history; without it, keep the Postal instance readable until its retention window has passed so past messages stay available while CamelMailer takes new traffic.Set up your domain and DNS
Add each sending domain to the CamelMailer server, then publish the three TXT records it hands you: a verification record, an SPF include, and the per-domain DKIM record. Because CamelMailer generates a fresh DKIM key, publish its new DKIM record alongside the Postal one and leave both live through the cutover so mail stays signed on either path. The full flow, including the selector and how verification works, is in Sending domains.
The send call
The request barely changes. The endpoint moves from /api/v1/send/message to /api/v2/server/messages, the authentication header stays X-Server-API-Key, and the one field rename is plain_body to text_body.
POST https://postal.example.com/api/v1/send/message
X-Server-API-Key: <your Postal server key>
Content-Type: application/json
{
"from": "billing@acme.com",
"to": ["ada@example.com"],
"subject": "Your receipt",
"html_body": "<p>Thanks for your purchase.</p>",
"plain_body": "Thanks for your purchase."
}POST https://app.camelmailer.com/api/v2/server/messages
X-Server-API-Key: <your CamelMailer server key>
Content-Type: application/json
{
"from": "billing@acme.com",
"to": ["ada@example.com"],
"subject": "Your receipt",
"html_body": "<p>Thanks for your purchase.</p>",
"text_body": "Thanks for your purchase."
}A successful send returns 201 Created with one entry per recipient. The request, response, batch sends, attachments, and custom headers are covered in Sending email.
SMTP drop-in
If your application already relays through Postal over SMTP, point it at CamelMailer instead and change nothing else. Create an SMTP-type credential on the server and use its key as the password:
Host: the SMTP hostname of your CamelMailer installation Port: 587 (STARTTLS) or 25 (plain with STARTTLS); 465 for implicit TLS Auth: AUTH PLAIN or AUTH LOGIN User: any value (accepted but unused for these mechanisms) Pass: an SMTP-type credential key from the server
SMTP submission reaches the same pipeline as the HTTP API and applies the same From-address authorization. See SMTP for the session details.
Cutover checklist
- Stand up CamelMailer next to Postal with a fresh PostgreSQL.
- Point it at your existing
postal.yml(or setPOSTAL_CONFIG_FILE_PATH), then create the server and a server API credential. - Add each sending domain and publish its verification, SPF, and DKIM records; keep the old Postal DKIM record live too.
- Verify each domain and confirm the health check is green.
- Swap the endpoint to
/api/v2/server/messages(or the SMTP host), renameplain_bodytotext_body, and drop in the new API key. - Send a test message and confirm it is accepted and delivered.
- Move senders across one credential at a time while both systems run in parallel, watching stats and bounces.
- Flip the remaining traffic, then keep Postal warm briefly before you decommission it.
