Markdown

Send email from Rust

Send email from any Rust app with Groundmail. For most apps, use the Rust SDK. This guide covers the alternatives: SMTP with lettre, or the REST API with reqwest.

Updated Nov 5, 20261 min read

SMTP (lettre) REST API (reqwest)
Best for Existing code that already sends over SMTP New code, retries, delivery status
Credential SMTP credential (gmsmtp_…) API key (gm_…)
Response 250 from the server Message ID (msg_…) and status, structured errors
Idempotency X-Groundmail-Idempotency-Key header Idempotency-Key header

Both use the same verified domains, suppressions, limits and webhooks.


Before you start

  1. Add and verify your sending domain. To test before DNS is set up, send from noreply@mail.groundmail.dev to your own account email.
  2. Create the credential you need at app.groundmail.dev: an SMTP credential or an API key.
  3. Keep it out of source control. Read it from an environment variable.

Send with SMTP and lettre

lettre is Rust's most widely used email library.

Terminal
cargo add lettre@0.11 --no-default-features --features builder,hostname,smtp-transport,tokio1,tokio1-rustls-tls,ring,rustls-native-certs
cargo add tokio --features macros,rt-multi-thread
Setting Value
Host smtp.groundmail.dev
Port 587
Security STARTTLS
Username groundmail
Password Your SMTP credential (gmsmtp_…)
use lettre::message::{header::ContentType, Mailbox, MultiPart, SinglePart};
use lettre::transport::smtp::authentication::Credentials;
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let from: Mailbox = "Acme <billing@yourdomain.com>".parse()?;
    let to: Mailbox = "user@example.com".parse()?;

    let email = Message::builder()
        .from(from)
        .to(to)
        .subject("Your receipt")
        .multipart(
            MultiPart::alternative()
                .singlepart(SinglePart::builder().header(ContentType::TEXT_PLAIN).body("Thanks for your order.".to_owned()))
                .singlepart(SinglePart::builder().header(ContentType::TEXT_HTML).body("<p>Thanks for your order.</p>".to_owned())),
        )?;

    let password = std::env::var("GROUNDMAIL_SMTP_PASSWORD")?;
    let mailer = AsyncSmtpTransport::<Tokio1Executor>::starttls_relay("smtp.groundmail.dev")?
        .port(587)
        .credentials(Credentials::new("groundmail".to_owned(), password))
        .build();

    mailer.send(email).await?;
    Ok(())
}

To send many messages, reuse one transport: it keeps a pool of connections.


Send with reqwest and the REST API

Terminal
cargo add reqwest --no-default-features --features json,rustls-tls
cargo add serde_json
cargo add tokio --features macros,rt-multi-thread
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api_key = std::env::var("GROUNDMAIL_API_KEY")?;

    let response = reqwest::Client::new()
        .post("https://api.groundmail.dev/emails")
        .bearer_auth(api_key)
        .header("Idempotency-Key", "receipt-order-1042")
        .json(&json!({
            "from": "Acme <billing@yourdomain.com>",
            "to": "user@example.com",
            "subject": "Your receipt",
            "html": "<p>Thanks for your order.</p>",
        }))
        .send()
        .await?;

    let status = response.status();
    let body: Value = response.json().await?;
    if !status.is_success() {
        return Err(format!("{status}: {}", body["error"]["message"]).into());
    }

    println!("{} {}", body["id"], body["status"]);
    Ok(())
}

Retrying with the same Idempotency-Key returns the first result instead of sending twice. See the Send email API for every field.


Next steps

  • Webhooks: get delivery and bounce events.
  • SMTP: headers, limits and errors.
  • Domains: verify your sending domain.

Was this page helpful?