# 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`](https://crates.io/crates/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](https://www.mailblastr.com/docs/domains/managing))
2. An **API key** (`mb_...`), read from `MAILBLASTR_API_KEY`. ([Authentication](https://www.mailblastr.com/docs/authentication))
3. **Rust 1.75 or newer** — that is the `rust-version` declared by the crate.
4. An **async runtime**. The examples use [Tokio](https://tokio.rs); any executor that can drive `reqwest` futures works.

## Install

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

Or pin it in `Cargo.toml` yourself:

**Cargo.toml**

```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**

```rust
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.

> **Warning:** 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](https://www.mailblastr.com/docs/api/emails-get) later or to correlate [webhook](https://www.mailblastr.com/docs/webhooks/overview) 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.

```rust
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.

```rust
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).

```rust
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();
```

> **Note:** 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.

> **Warning:** 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](https://www.mailblastr.com/docs/api/emails-send) reference for every body field (`cc`, `bcc`, `reply_to`, `attachments`, `scheduled_at`, templates and variables).
- Explore [the SDKs reference](https://www.mailblastr.com/docs/resources/sdks) 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](https://www.mailblastr.com/docs/webhooks/overview).
- Prefer no crate at all? [Send with Rust over raw HTTP](https://www.mailblastr.com/docs/send-with/rust) posts JSON with `reqwest` directly.
