It sends in waves, best readers first
A newsletter starts with 100 of your most engaged readers. Each wave doubles, ten minutes after the last one, up to 10,000 at a time. If the first wave goes badly, the rest of your list never sees it.
Mailrust is an open-source email platform you run on your own server. Write newsletters for your contacts in a dashboard, and send transactional emails from your app with an API. Every email goes out through your own Amazon SES account.
Newsletters go out in bulk to a contact list. Transactional emails are personal and triggered by your app: login codes, receipts, alerts. Mailrust sends both from one queue and one block list, so a big newsletter never holds up a login code.
Write in a block editor and type / to add images, buttons and merge tags. Before it sends, Mailrust checks the newsletter. Then it mails your most engaged readers first and grows each wave from there.
Hi {first_name}, the new studio has a door wide enough for the kiln at last. Here is what the first firing looked like.
Send login codes, receipts and alerts with POST /emails. Mark them urgent and they skip ahead of any newsletter in the queue. Retry with the same idempotency key and the email still goes out once.
# send a login code curl https://mail.example.com/emails \ -H "Authorization: Bearer $MAILRUST_KEY" \ -H "Idempotency-Key: login-ana-4812" \ -H "Content-Type: application/json" \ -d '{ "from": "Acme <login@acme.dev>", "to": "ana@example.com", "subject": "Your login code", "text": "Your code is 481 207", "priority": "urgent" }'
// send a login code await fetch("https://mail.example.com/emails", { method: "POST", headers: { Authorization: `Bearer ${process.env.MAILRUST_KEY}`, "Idempotency-Key": "login-ana-4812", "Content-Type": "application/json", }, body: JSON.stringify({ from: "Acme <login@acme.dev>", to: "ana@example.com", subject: "Your login code", text: "Your code is 481 207", priority: "urgent", }), });
# send a login code requests.post( "https://mail.example.com/emails", headers={ "Authorization": f"Bearer {os.environ['MAILRUST_KEY']}", "Idempotency-Key": "login-ana-4812", }, json={ "from": "Acme <login@acme.dev>", "to": "ana@example.com", "subject": "Your login code", "text": "Your code is 481 207", "priority": "urgent", }, )
// send a login code reqwest::Client::new() .post("https://mail.example.com/emails") .bearer_auth(std::env::var("MAILRUST_KEY")?) .header("Idempotency-Key", "login-ana-4812") .json(&json!({ "from": "Acme <login@acme.dev>", "to": "ana@example.com", "subject": "Your login code", "text": "Your code is 481 207", "priority": "urgent" })) .send() .await?;
{ "id": "0192f3c1-…", "priority": "urgent" }Your list stays in your database. Your mail goes out through your SES account. Nothing in between is hidden.
An email platform runs for months and must never drop a message or send one twice. These are the parts of Mailrust that Rust makes hard to get wrong.
Every database query is checked against the real schema when the code compiles. A typo in a column name fails the build before it can fail a send.
sqlx query! macrosThe engine has zero unsafe blocks. There is no garbage collector, so a large send has no pauses in the middle.
One async process runs the API, the send worker, the newsletter waves, webhook delivery, CSV imports and the SES quota sync. Postgres is the queue, so there is no Redis to run.
tokio · SKIP LOCKEDThe API rejects fields it doesn't know. Send "prority" by mistake and you get an error back, not a silently ignored setting.
Connections use rustls, a TLS library written in Rust. There is no OpenSSL in the engine to patch.
rustlsEvery change passes Clippy with warnings treated as errors, and cargo-deny checks each dependency against the RustSec advisory list.
clippy -D warnings · cargo-denyThe OpenAPI reference is generated from the same Rust types the API uses, so the docs match the code.
GET /openapi.jsonThis is the full list for v0.3.1. Nothing here is a roadmap item.
The newsletter editor.
/ to add any blockYour subscribers, in your database.
The checks between "Send" and the inbox.
For the email your app sends.
POST /emails for one emailYour Amazon SES account, handled with care.
Numbers you can trust.
What it takes on your server.
.eml files, so you can try it without AWSMost email trouble starts after you press Send. These are the parts of Mailrust that deal with it, and you can try each one here.
A newsletter starts with 100 of your most engaged readers. Each wave doubles, ten minutes after the last one, up to 10,000 at a time. If the first wave goes badly, the rest of your list never sees it.
Mailrust watches every bounce and complaint SES reports back. Past 2% bounces or 0.08% complaints, it pauses the account before your sender reputation takes the damage. Drag the slider to see where the line is.
Every email has a lane: urgent, normal or bulk. Urgent mail goes first, and the last 10% of your daily SES quota is kept for it. If a big newsletter uses up the day, your password resets still go out.
The Send button stays locked until the newsletter passes every check. Untick one to see what happens.
Your app and your dashboard put email into the same Postgres queue. Mailrust sends it through your SES account and records what happens to each one.
Calls POST /emails with an API key.
You write and press Send in the dashboard.
Checks, queues and paces every email.
Signs with your DKIM and sends from your domain.
Gmail, Outlook, iCloud and the rest.
SES reports every delivery, bounce and complaint. Mailrust checks the signature, updates the block list, pauses the account if a rate gets too high, fills in your reports and tells your app through a signed webhook.
To run Mailrust in production you need a server with Docker, a domain you control and an Amazon SES account. To try it on your laptop you need none of them: until you add SES keys, Mailrust writes each email to a folder as an .eml file.
$ git clone https://github.com/irfanfaraaz/mailrust $ cd mailrust && cp .env.example .env $ docker compose up -d postgres $ cargo run -- migrate $ cargo run -- serve
# Postgres, Mailrust, the dashboard, # Caddy with HTTPS and nightly backups $ cd deploy $ docker compose -f docker-compose.prod.yml \ --env-file ../.env up -d
A server, a domain and an Amazon SES account. New SES accounts start in a sandbox, so ask AWS for production access early.
DNS records for DKIM and MAIL FROM. Mailrust gives you the exact records and checks them until they pass.
Upgrades are a pull and a migrate. Backups run every night on their own.
Mailrust is new. Here is exactly what has shipped, and the things it does not do today, so you can decide with the full picture.
Newsletter reports with spam complaints, local time zones everywhere, toasts for every action, and a round of fixes.
Newsletters: the block editor, waves, checks before send, reports, the public archive and image uploads.
The dashboard and your audience: contacts, tags, signup form, CSV import and export, unsubscribe.
The sending API and the engine: queue, SES sender, priority lanes, webhooks, tracking and auto-pause.
A server with Docker, a domain you control and an Amazon SES account. Postgres is the only other service, and the production compose file starts it for you along with the dashboard, Caddy and nightly backups.
Yes. Mailrust sends through your SES account, so the sending reputation, the limits and the bill are yours. To try it locally you don't need AWS at all, because the fake sender writes each email to a file.
Yes. Each door works on its own. If you use both, they share one block list and one queue, and urgent API mail always goes ahead of a newsletter.
Resend is a sending API and beehiiv is a newsletter platform, and both run on their own servers. Mailrust does both jobs in one engine that runs on yours, with your SES account and your database.
The first wave is small and goes to your most engaged readers. If bounces pass 2% or complaints pass 0.08%, Mailrust pauses the account and the newsletter stops. When you resume, nobody who already got it gets it again.
Yes. Import a CSV of up to 100,000 rows. The import runs in the background and picks up where it stopped if the server restarts. You can export your contacts as CSV at any time.
You can run, change and use Mailrust for free, for any purpose. If you change the code and offer it to other people as a network service, you have to share your changes under the same licence.
Clone the repo and send your first email from your laptop. You don't need an AWS account to start.