Send emails with Rust
Send email from Rust with the official mailblastr crate — async, typed errors, and built on reqwest with rustls.
The official `mailblastr` crate is the fastest way to send email from Rust. Every method is async and returns Result<T, mailblastr::Error> (the crate re-exports mailblastr::Result<T> as the alias for that), so API failures arrive as typed values instead of raw HTTP.
You construct one Mailblastr client and reach every resource through a field on it — mailblastr.emails, mailblastr.contacts, mailblastr.campaigns, and so on. The client is Clone and internally reference-counted, so cloning it into tasks is cheap and there is no need for a global.
Prerequisites
- A verified domain for your
fromaddress — publish its SPF/DKIM/DMARC records first. (guide) - An API key (
mb_...), read fromMAILBLASTR_API_KEY. (Authentication) - Rust 1.75 or newer — that is the
rust-versiondeclared by the crate. - An async runtime. The examples use Tokio; any executor that can drive
reqwestfutures works.
Install
cargo add mailblastr
cargo add tokio -F macros,rt-multi-threadOr pin it in Cargo.toml yourself:
[dependencies]
mailblastr = "5"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }The crate pulls in reqwest with rustls-tls and default-features = false, so it does not need OpenSSL on the build machine.
Send an email
Read the key from the environment and hand it to Mailblastr::new. delivered@mailblastr.dev is MailBlastr's mailbox simulator — the send is accepted, produces a real email object and delivery event, and is intercepted before it reaches a provider, so it is safe to send to while you wire things up. Do not point a send at example.com: those recipients are suppressed and the call comes back 422 validation_error having sent nothing.
use mailblastr::{Mailblastr, Result, SendEmailOptions};
#[tokio::main]
async fn main() -> Result<()> {
let api_key = std::env::var("MAILBLASTR_API_KEY").expect("MAILBLASTR_API_KEY not set");
let mailblastr = Mailblastr::new(api_key);
let email = SendEmailOptions::new(
"Acme <hello@yourdomain.com>",
["delivered@mailblastr.dev"],
"Hello from Rust",
)
.with_html("<p>Sent with the mailblastr crate 🦀</p>");
let sent = mailblastr.emails.send(email).await?;
println!("Sent email {}", sent.id);
Ok(())
}SendEmailOptions::new(from, to, subject) takes the three required fields; everything optional is a chained with_* builder — with_text, with_cc, with_bcc, with_reply_to, with_preview_text, with_header, with_attachment, with_topic_id, with_template_id, with_variable, with_scheduled_at. to is any iterable of string-likes, so an array literal, a Vec<String>, or an iterator all work.
with_scheduled_at("2026-09-01T09:00:00Z")) must go to a real mailbox such as you@yourdomain.com. The simulator only intercepts immediate sends, so scheduling one to delivered@mailblastr.dev is rejected with 422 validation_error.Handling the response
A successful send resolves to CreateEmailResponse { id }. Keep the id to retrieve the email later or to correlate webhook events.
A non-2xx answer is Error::Api, which boxes an ApiError carrying the API's { statusCode, name, message } envelope as status_code, name and message. Use err.api() to reach it — that returns None for Error::Http (transport) and Error::Json (decode) failures. Branch on name together with status_code, never on message: message text is scrubbed server-side and is not a stable contract.
match mailblastr.emails.send(email).await {
Ok(sent) => println!("Sent email {}", sent.id),
Err(err) => match err.api() {
// The API answered with an error envelope.
Some(api) => {
eprintln!("{} {}: {}", api.status_code, api.name, api.message);
// Plan and quota rejections say WHICH allowance ran out.
if let Some(limit) = &api.limit {
eprintln!("{} cap hit: {}/{}", limit.kind, limit.used, limit.limit);
}
// Reputation gates say whether waiting will help.
if let Some(rep) = &api.reputation {
eprintln!("{} sending gated, retryable={}", rep.scope, rep.retryable);
}
}
// Transport or decode failure.
None => eprintln!("request failed: {err}"),
},
}limit and reputation are None on an ordinary error, sent is an empty Vec outside a part-way batch failure, and api.body holds the whole parsed body so a field newer than your crate version is still reachable.
Retry-safe sends
When your own code may retry a send — a job runner, a webhook handler — use the idempotent variant. Replaying the same key returns the stored response instead of sending a second copy. The key must be 1–255 characters (mailblastr::IDEMPOTENCY_KEY_MAX_LEN); the API answers anything else with 400 invalid_idempotency_key. Only POST /emails and POST /emails/batch honour the header.
let sent = mailblastr
.emails
.send_with_idempotency_key(email, &format!("welcome-{user_id}"))
.await?;Tuning the client
Mailblastr::new targets production with a 30s per-request timeout and up to 2 automatic retries. Use the builder to change any of that, or Mailblastr::with_client to bring your own reqwest::Client (proxies, connection pools — its own timeout then applies).
use std::time::Duration;
let mailblastr = Mailblastr::builder(api_key)
.base_url("https://www.mailblastr.com/api") // override the API host
.timeout(Duration::from_secs(10)) // per attempt; Duration::ZERO disables it
.max_retries(3) // 429/503 budget; 0 disables retrying
.build();User-Agent on every request (mailblastr-rust/5.0.0, exposed as mailblastr::USER_AGENT) — a raw request without one is rejected with 403 validation_error before it is even authenticated. It retries only 429 and 503, honouring Retry-After and otherwise backing off exponentially. Timeouts, network errors and other 5xx are never retried, so a send is never silently duplicated.mb_ API key from the environment (or your secrets manager) and keep it server-side. Never compile it into a binary you ship to users — anyone with the key can send email as your account.Next steps
- Send up to 100 messages in one request with
mailblastr.batch.send_emails(vec![BatchEmailOptions::new(...)]).BatchEmailOptionsisSendEmailOptionsminusattachmentsandscheduled_at— the API rejects both in a batch, so the type keeps that a compile error. The response isSendEmailBatchResponse { data }. - See the full Send Email API reference for every body field (
cc,bcc,reply_to,attachments,scheduled_at, templates and variables). - Explore the SDKs reference for the other resources the crate exposes — contacts, segments, topics, campaigns, templates, automations, webhooks, events, logs and API keys.
- Verify inbound deliveries with
mailblastr::verify_webhook_signature(raw_body, &WebhookHeaders::new(id, timestamp, signature), secret, &VerifyWebhookOptions::default())— pass the raw request body, never re-serialized JSON. See Webhooks. - Prefer no crate at all? Send with Rust over raw HTTP posts JSON with
reqwestdirectly.