Browse documentation
Documentation

Mail

Edit this guide on GitHub

Declare email with mail! and deliver it with send. A Transport determines how messages are delivered.

Everything below is re-exported from hearth::mail and gated behind the mail feature. The SMTP transport additionally needs the mail-smtp feature.

# Cargo.toml
[dependencies]
hearth = { version = "0.1.0", features = ["mail", "mail-smtp"] }

Setup

Choose a transport in MailConfig and register it with the router's mail method:

use hearth::{
    mail::{FileTransport, MailConfig, RouterBuilderMailExt},
    router::{Router, RouterBuilderDiscoverExt, module_router},
};

pub fn router() -> Router {
    module_router!()
        .discover()
        .mail(
            MailConfig::builder()
                .transport(FileTransport::new("target/mail"))
                .build(),
        )
        .build()
}

Handlers use the registered transport. You can change delivery settings without changing the code that sends messages.

Declaring and sending mail

The mail! macro creates a Mail from name: value fields. Its html field accepts view! markup:

// src/app/api/welcome.rs
use hearth::{
    Result,
    context::Cx,
    mail::{mail, send},
    router::route,
};

#[route(POST)]
async fn welcome(cx: &Cx) -> Result<&'static str> {
    let mail = mail! {
        from: ("Hearth", "welcome@example.com"),
        to: "ada@example.com",
        subject: "Welcome, Ada!",
        html: {
            <h1>"Welcome!"</h1>
            <p>"Your account is ready."</p>
        },
    }?;

    send(cx, mail).await?;

    Ok("sent")
}

Addresses accept strings, (name, address) pairs, or Mailbox values. Recipient fields also accept collections. Hearth derives a plain-text body from the HTML by default. See mail! for field details or use MailBuilder to create a message without the macro.

send returns a Receipt with the message's Message-ID. Use that ID in in_reply_to and references to link a later reply. The receipt confirms that the transport accepted the message. It does not confirm inbox delivery.

Sending returns a SendError if the message lacks a sender, recipients, or body, or if delivery fails.

Attachments

Add files with the attachments field. Use Attachment::new for a downloadable file or Attachment::inline for content referenced by a cid: URL in the HTML:

let mail = mail! {
    subject: "Your invoice",
    html: {
        cx =>
        <img src="cid:logo" alt="Example logo">
        <p>"The invoice is attached."</p>
    },
    attachments: [
        Attachment::inline("logo", "image/png", b"\x89PNG"),
        Attachment::new("invoice.pdf", "application/pdf", b"%PDF-"),
    ],
}?;

Transports

Choose a transport for your delivery environment, or implement Transport for your own service.

SMTP

SmtpTransport submits messages to an SMTP server and reuses connections across sends. Enable mail-smtp to use it. relay uses implicit TLS on port 465, and starttls uses STARTTLS on port 587. You can also configure the connection with a URL:

use hearth::mail::SmtpTransport;

let explicit = SmtpTransport::relay("smtp.example.com")?
    .credentials("username", "password")
    .build();

let from_url = SmtpTransport::from_url("smtps://user:pass@smtp.example.com:465")?.build();

Files during development

FileTransport writes each message to an .eml file for local inspection. Filenames include the send time.

Memory in tests

MemoryTransport captures messages for tests. Clones share the captured messages, so the test can inspect what the application sent:

use hearth::{
    context::CxTestBuilder,
    mail::{MailConfig, MemoryTransport, Mailbox, mail, send},
};

let transport = MemoryTransport::new();
let config = MailConfig::builder().transport(transport.clone()).build();
let cx = CxTestBuilder::new().app_context(config).build();

send(&cx, mail! { from: "ada@example.com", to: "bob@example.com", text: "Hi" }?).await?;

assert_eq!(transport.sent().len(), 1);
assert_eq!(transport.sent()[0].to(), [Mailbox::new("bob@example.com")?]);

The memory transport validates message content before recording it. It does not check whether an external service would accept the message.

Custom transports

Implement Transport to deliver through anything else, such as a mail provider's HTTP API. Mail::formatted renders the RFC 5322 wire form for APIs that accept raw messages.