Source: https://cli.nylas.com/guides/send-email-rust

# Send Email in Rust: lettre and Email APIs

Rust has three practical paths to sending email: the lettre crate over SMTP, reqwest against provider REST APIs, and std::process::Command calling a CLI that handles OAuth. Runnable code for each, compared in one table.

Written by [Nick Barraclough](https://cli.nylas.com/authors/nick-barraclough) Product Manager

Reviewed by [Qasim Muhammad](https://cli.nylas.com/authors/qasim-muhammad)

Updated June 9, 2026

> **TL;DR:** The lettre crate sends over SMTP with a 16-character app password (~5 min setup). reqwest hits the Gmail REST API directly but leaves OAuth token management to you (~15+ min). A `std::process::Command` call to the CLI covers 6 providers in about 15 lines of Rust with zero email crates. The comparison table at the end shows which one survives a Gmail-to-Outlook switch without a single code change.

> **Disclosure:** Nylas CLI is built by Nylas, Inc. This comparison reflects our testing and product understanding as of June 9, 2026.

The commands in this guide are documented at: [`nylas email send`](https://cli.nylas.com/docs/commands/email-send) for the std::process subprocess pattern, [`nylas auth login`](https://cli.nylas.com/docs/commands/auth-login) for OAuth login, and [`nylas auth config`](https://cli.nylas.com/docs/commands/auth-config) for API-key setup without a browser.

## How do you send email in Rust?

To send email in Rust you pick one of three transports: the lettre crate speaking SMTP, the reqwest HTTP client calling a provider REST API like Gmail's, or `std::process::Command` shelling out to a CLI binary that owns authentication and delivery. Each compiles to a small binary; they differ in setup time and provider reach.

The numbers frame the choice. lettre has passed 11 million all-time downloads on crates.io as of June 2026, making it the default SMTP answer. reqwest sits above 519 million downloads because it's the general-purpose HTTP client, not an email tool — you assemble the email plumbing yourself. The subprocess route needs no crates at all beyond `serde_json` for parsing output.

## How do you send email with the lettre crate?

lettre is Rust's standard mailer library: it builds RFC-compliant messages and delivers them over SMTP with TLS. The current stable release is 0.11.22, published May 2026, and the [lettre docs on docs.rs](https://docs.rs/lettre) cover both blocking and tokio-async transports. Gmail setup takes about 5 minutes with an app password.

The example below uses `SmtpTransport::relay`, which connects to port 465 over implicit TLS; call `starttls_relay` instead for port 587. A 16-character [app password](https://support.google.com/accounts/answer/185833) is the credential here — Less Secure Apps access ended in May 2022 for personal Google accounts and on May 1, 2025 for Workspace. Personal Gmail also caps sending at 500 messages per day.

```rust
use lettre::message::Message;
use lettre::transport::smtp::authentication::Credentials;
use lettre::{SmtpTransport, Transport};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let email = Message::builder()
        .from("Your Name <you@gmail.com>".parse()?)
        .to("recipient@example.com".parse()?)
        .subject("Quarterly report")
        .body(String::from("See the attached spreadsheet."))?;

    let creds = Credentials::new(
        "you@gmail.com".to_string(),
        "your-16-char-app-password".to_string(),
    );

    let mailer = SmtpTransport::relay("smtp.gmail.com")?
        .credentials(creds)
        .build();

    mailer.send(&email)?;
    println!("Message sent");
    Ok(())
}
```

SMTP itself is defined by [RFC 5321](https://datatracker.ietf.org/doc/html/rfc5321), and lettre hides most of its sharp edges — connection negotiation, AUTH mechanisms, and message encoding. What it can't hide is that credentials are provider-specific: moving from Gmail to Outlook means a new host, a new auth story, and since Microsoft retired SMTP Basic Auth for many tenants, possibly no SMTP path at all.

## How do you send email with reqwest and the Gmail API?

reqwest sends email by POSTing a base64url-encoded RFC 2822 message to the Gmail REST endpoint `users.messages.send`. There's no official Google SDK for Rust, so you build the request manually with reqwest (519M+ downloads, current stable 0.13) plus the `base64` and `serde_json` crates. Expect 15+ minutes of OAuth setup first.

The code below reads an OAuth access token from an environment variable and calls the endpoint documented in the [Gmail API reference](https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.messages/send). Google charges 100 quota units per `messages.send` call, and the raw message must use the URL-safe base64 alphabet — encode with the standard `+/` characters and the API returns a 400. Padding is optional; the example uses the unpadded variant.

```rust
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use serde_json::json;

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

    let raw = [
        "To: recipient@example.com",
        "Subject: Quarterly report",
        "Content-Type: text/plain; charset=utf-8",
        "",
        "See the attached spreadsheet.",
    ]
    .join("\r\n");

    let res = reqwest::Client::new()
        .post("https://gmail.googleapis.com/gmail/v1/users/me/messages/send")
        .bearer_auth(access_token)
        .json(&json!({ "raw": URL_SAFE_NO_PAD.encode(raw) }))
        .send()
        .await?;

    println!("Status: {}", res.status());
    Ok(())
}
```

The hard part isn't this request — it's the token. Access tokens expire after 3,600 seconds, so a real program also implements the refresh-token exchange, storage, and retry logic that Google's official SDKs handle in other languages. And the result is Gmail-only: sending through Outlook means rewriting against [Microsoft Graph's sendMail](https://learn.microsoft.com/en-us/graph/api/user-sendmail), a different endpoint with a different payload shape.

## How do you send email with std::process::Command?

`std::process::Command` sends email by spawning the CLI binary, which owns OAuth token refresh, MIME encoding, and provider differences for Gmail, Outlook, Exchange, Yahoo, iCloud, and IMAP. Rust's standard library [Command API](https://doc.rust-lang.org/std/process/struct.Command.html) captures stdout and the exit status, so the whole integration is about 15 lines with no email crates.

Setup takes roughly 2 minutes: install the binary with `brew install nylas/nylas-cli/nylas` (other methods are in the [getting started guide](https://cli.nylas.com/guides/getting-started)), then run `nylas auth login` once to complete the browser OAuth flow. After that, one command sends a message from any connected account.

```bash
brew install nylas/nylas-cli/nylas
nylas auth login
nylas email send --to recipient@example.com --subject "Test" --body "Hello from Rust" --yes
```

The Rust wrapper below passes `--yes` to skip the confirmation prompt and `--json` to get structured output for `serde_json`. A non-zero exit status surfaces stderr as the error, so auth failures and network errors propagate like any other `Result`. Each invocation forks a new process, about 100ms slower than holding an open SMTP connection.

```rust
use std::process::Command;

fn send_email(to: &str, subject: &str, body: &str) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
    let output = Command::new("nylas")
        .args([
            "email", "send",
            "--to", to,
            "--subject", subject,
            "--body", body,
            "--yes", "--json",
        ])
        .output()?;

    if !output.status.success() {
        return Err(String::from_utf8_lossy(&output.stderr).into());
    }
    Ok(serde_json::from_slice(&output.stdout)?)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = send_email(
        "recipient@example.com",
        "Quarterly report",
        "See the attached spreadsheet.",
    )?;
    println!("Sent: {response}");
    Ok(())
}
```

## How do the three Rust email methods compare?

lettre wins on raw throughput, reqwest wins on control, and the CLI subprocess wins on setup time and provider coverage. The table compares the three across 6 dimensions; lines of code count the minimum to send one plaintext message, including imports and auth handling, and setup time assumes a Gmail account with 2-Step Verification enabled.

| Dimension | lettre (SMTP) | reqwest (REST) | CLI subprocess |
| --- | --- | --- | --- |
| Setup time | ~5 minutes | ~15+ minutes | ~2 minutes |
| Auth method | App password / SMTP creds | OAuth 2.0 (you manage tokens) | OAuth 2.0 (CLI manages tokens) |
| Providers | Any SMTP server | One API per provider | Gmail, Outlook, Exchange, Yahoo, iCloud, IMAP |
| Crates needed | lettre (1) | reqwest, tokio, base64, serde_json (4) | serde_json (1) |
| Lines of code | ~25 | ~30 + token refresh | ~15 |
| Batch performance | Reused connection (fast) | HTTP connection pooling | Process-per-send (~100ms overhead) |

The provider row is the one that bites later. A lettre integration written for Gmail breaks the day your team moves to a Microsoft 365 tenant with SMTP AUTH disabled, and a reqwest integration needs a second code path for Graph. The subprocess version survives that switch with zero code changes — only the connected grant differs. For batches past 1,000 messages, lettre's persistent connection beats spawning 1,000 processes; for everything else, the 2-minute path wins. The same trade-off plays out in [Node.js](https://cli.nylas.com/guides/send-email-nodejs) and [Python](https://cli.nylas.com/guides/send-email-python).

## Next steps

- [Send email from Python](https://cli.nylas.com/guides/send-email-python) — the same 3-method comparison with smtplib and subprocess
- [Send email from Node.js](https://cli.nylas.com/guides/send-email-nodejs) — Nodemailer, Gmail API client, and child_process
- [Send email with curl](https://cli.nylas.com/guides/send-email-with-curl) — the raw REST request without any language runtime
- [Twilio SendGrid vs Nylas](https://cli.nylas.com/guides/twilio-vs-nylas) — transactional relay vs mailbox API for app email
- [EmailEngine vs Nylas](https://cli.nylas.com/guides/emailengine-vs-nylas) — self-hosted IMAP gateway vs hosted email API
- [Full command reference](https://cli.nylas.com/docs/commands) — every flag and subcommand documented
- [lettre documentation](https://docs.rs/lettre) — transport options, async support, and message building
- [reqwest documentation](https://docs.rs/reqwest) — client configuration, TLS, and connection pooling
- [Gmail API messages.send reference](https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.messages/send) — payload format and quota costs
