Send with your stack

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

  1. A verified domain for your from address — publish its SPF/DKIM/DMARC records first. (guide)
  2. An API key (mb_...), read from MAILBLASTR_API_KEY. (Authentication)
  3. Rust 1.75 or newer — that is the rust-version declared by the crate.
  4. An async runtime. The examples use Tokio; any executor that can drive reqwest futures works.

Install

cargo add mailblastr
cargo add tokio -F macros,rt-multi-thread

Or pin it in Cargo.toml yourself:

Cargo.toml
[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.

src/main.rs
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.

A scheduled send (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();
The crate sends the required 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.
Read the 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(...)]). BatchEmailOptions is SendEmailOptions minus attachments and scheduled_at — the API rejects both in a batch, so the type keeps that a compile error. The response is SendEmailBatchResponse { 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 reqwest directly.