Joshua Cooper I'm a full-stack web developer with a passion for well-written and efficient code. On the frontend, I use Typescript with React and on the backend I use Rust or Node.js with Typescript.

Email crates for Rust: lettre and imap

6 min read 1693

Email Crates for Rust: lettre and imap

Most web applications these days need to interact with email in some way or another, so good language support is crucial. Rust certainly has basic support for working with emails, but I wouldn’t say it can cover all use cases just yet. The ecosystem is quite small and doesn’t currently have good support for async/await.

That said, it’s a useful exercise to mess around with the solutions that are available and see how they work. In this tutorial, we’ll show you how to send emails and connect to IMAP servers in Rust.

Creating emails with lettre

The most popular crate for sending emails is lettre. As of now, the latest stable release of letter (0.9) doesn’t include an async API, but there’s also an alpha release for version 0.10, which supports async via tokio and async-std.

Let’s first take a look at the current version without async then port the code to an async implementation using the alpha release.

Add the following to Cargo.toml to get started.

[dependencies]
lettre = "0.9"
lettre_email = "0.9"

With version 0.9, we need to use lettre_email to build emails then lettre to send them, so we need specify both.

Start by creating a basic email.

use lettre_email::EmailBuilder;

fn main() {
    let email = EmailBuilder::new()
        .to("hello@example.com")
        .from("me@hello.com")
        .subject("Example subject")
        .text("Hello, world!")
        .build()
        .unwrap();
}

This uses the builder pattern to create the data structure that represents an email. If we want to specify a name and an address, we can use a tuple, like this:

let email = EmailBuilder::new()
    .to(("hello@example.com", "Alice Smith"))
    .from(("me@hello.com", "Bob Smith"))
    .subject("Example subject")
    .text("Hello, world!")
    .build()
    .unwrap();

Adding a HTML body is as simple as calling the corresponding method on the builder.

let email = EmailBuilder::new()
    .to("hello@example.com")
    .from("me@hello.com")
    .subject("Example subject")
    .html("<h1>Hello, world!</h1>")
    .build()
    .unwrap();

Let’s say we want to send attachments with our emails. Here’s how you would attach a local file to an email:

We made a custom demo for .
No really. Click here to check it out.

use lettre_email::mime;
use lettre_email::EmailBuilder;
use std::path::Path;

fn main() {
    let email = EmailBuilder::new()
        .to("hello@example.com")
        .from("me@hello.com")
        .subject("Example subject")
        .html("<h1>Hello, world!</h1>")
        .attachment_from_file(
            &Path::new("path/to/file.pdf"), // Path to file on disk
            Some("Cookie-recipe.pdf"),      // Filename to use in the email
            &mime::APPLICATION_PDF,
        )
        .unwrap()
        .build()
        .unwrap();
}

Alternatively, we can use the attachment(...) builder method to send a vector of bytes directly from memory instead of a file on disk.

There are a few more builder methods for things I haven’t mentioned, so if you want to do something else with the email, take a look at the docs for lettre_email::EmailBuilder.

Sending emails with lettre

With lettre, we can use anything implementing the lettre::Transport trait to send the emails we created earlier.

The easiest transport to get started with is StubTransport. As the name suggests, this is just a stub, not a real transport. This is most useful for testing.

use lettre::stub::StubTransport;
use lettre::Transport;
use lettre_email::EmailBuilder;

fn main() {
    let email = EmailBuilder::new()
        .to("hello@example.com")
        .from("me@hello.com")
        .subject("Example subject")
        .html("<h1>Hello, world!</h1>")
        .build()
        .unwrap();

    let mut mailer = StubTransport::new_positive();

    let result = mailer.send(email.into());

    println!("{:?}", result);
}

Since we used new_positive() to create the stub transport, the send(...) method will always succeed. If you want to force sending to fail, look at StubTransport::new(...) in the documentation.

To adapt this to actually send the emails, we just need to replace the stub mailer with a real transport. For this example, let’s use the SmtpTransport.

Since the SMTP transport has more configuration options than a stub, it uses a builder called SmtpClient.

use lettre::smtp::authentication::Credentials;
use lettre::{SmtpClient, Transport};
use lettre_email::EmailBuilder;

fn main() {
    let email = EmailBuilder::new()
        .to("hello@example.com")
        .from("me@hello.com")
        .subject("Example subject")
        .html("<h1>Hello, world!</h1>")
        .build()
        .unwrap();

    let mut mailer = SmtpClient::new_simple("smtp.hello.com")
        .unwrap()
        .credentials(Credentials::new("username".into(), "password".into()))
        .transport();

    let result = mailer.send(email.into());

    println!("{:?}", result);
}

Now this will actually send the email over the SMTP server you connect to. Here, we created a client using new_simple(...), which builds an encrypted transport using the provided domain to validate TLS certificates. This is the recommended way to create an SmtpTransport. Lucky for us, it’s also the easiest. After that, we just passed some credentials then sent the email, like with a stub.

Porting to async/await

If you’re happy living on the bleeding edge, you might want to try out the alpha version of lettre for async support. Version 0.10 of lettre will also contain the email builder under a feature flag, so we don’t need to use lettre_email anymore.

First, change your Cargo.toml dependencies.

[dependencies]
lettre = { version = "0.10.0-alpha.2", features = ["builder", "tokio02-native-tls"] }
tokio = { version = "0.2", features = ["macros"] }

Note that we enabled the optional features for building emails and tokio runtime support.

Now the code to create and send an email looks like this:

use lettre::transport::smtp::authentication::Credentials;
use lettre::{AsyncSmtpTransport, Message, Tokio02Connector, Tokio02Transport};

#[tokio::main]
async fn main() {
    env_logger::init();

    let email = Message::builder()
        .to("hello@example.com".parse().unwrap())
        .from("me@hello.com".parse().unwrap())
        .subject("Example subject")
        .body("Hello, world!")
        .unwrap();

    let mailer = AsyncSmtpTransport::<Tokio02Connector>::relay("smtp.hello.com")
        .unwrap()
        .credentials(Credentials::new("username".into(), "password".into()))
        .build();

    let result = mailer.send(email).await;

    println!("{:?}", result);
}

The main changes here are that we replaced lettre_email::EmailBuilder with lettre::Message and lettre::SmtpClient with lettre::AsyncSmtpTransport. Be careful with the other breaking changes in this version. For example, we now need to parse addresses before passing them to to(...) and from(...) in the message builder functions.

Also, be aware that lettre 0.10 is an alpha release and doesn’t yet have support for everything that 0.9 does. Most applications will be fine without async, so feel free to stick with the synchronous version that we saw earlier until this one is stable.

IMAP

Sending emails is more important for many applications, but you may encounter the need to monitor and interact with incoming emails. The best way to do that in Rust right now is to use the imap crate. Like the current version of lettre, this crate does not provide an async API.

To get started, create a new project with these dependencies.

[dependencies]
imap = "2"
native-tls = "0.2"

Next, connect to an IMAP server.

use native_tls::TlsConnector;

fn main() {
    let domain = "imap.example.com";
    let tls = TlsConnector::builder().build().unwrap();
    let client = imap::connect((domain, 993), domain, &tls).unwrap();
}

We need to pass the domain once as the address to connect to and again as the domain to validate the TLS certificate against.

The client is unauthenticated, so let’s log in next.

let mut imap_session = client.login("hello@example.com", "password").unwrap();

Now we have an imap::Session we can use to interact with our remote mailboxes.

As an example let’s read the body of the first five emails in our inbox.

imap_session.select("INBOX").unwrap();

let messages = imap_session.fetch("1,2,3,4,5", "RFC822").unwrap();

for message in messages.iter() {
    if let Some(body) = message.body() {
        println!("{}", std::str::from_utf8(body).unwrap());
    } else {
        println!("Message didn't have a body!");
    }
}

imap_session.logout().unwrap();

First, we select the mailbox we want to work with — in this case, "INBOX". Then, we fetch message numbers 1 to 5 in the inbox. The "RFC822" you can see in the fetch method dictates the format of the email body. Once we get a response containing messages we iterate over them, we extract each body and print it. Finally, we end our session by logging out.

See the Fetch type for documentation on what else you can get out of the messages.

In addition to reading emails, let’s walk through how to manage your mailboxes.

To create and delete them:

imap_session.create("work").unwrap();
imap_session.delete("work").unwrap();

Creation will fail if you try to create "INBOX" or an already existing mailbox. Likewise, deletion will fail if you try to delete "INBOX" or a mailbox that doesn’t exist.

Finally, let’s see how to monitor mailboxes for changes.

imap_session.select("INBOX").unwrap();
imap_session.idle().unwrap().wait();

Here, idle() will return a Handle that we can wait() on. This will block until the selected mailbox changes. It’s also possible to wait with a keepalive using wait_keepalive() and block with a timeout using wait_with_timeout(Duration). The ability to block until changes are detected is helpful because it helps us avoid polling in a slower loop that would comsume more resources.

In my opinion, imap has a nice API that’s easy to wrap your head around. If you read through the documentation for Session, that should give you a good idea of what else this crate can do.

Keep in mind that most of the methods used in these examples make a network request, so they can and will fail. For the sake of simplicity, I’ve used unwrap() liberally, but you should definitely handle errors in a real application.

Conclusion

Overall, Rust’s email support isn’t outstanding — yet. Sending emails without async/await has the best support, so if that’s all you need, you might be fine to use it already. If you want to deal with incoming email, the imap crate is your best bet.

I didn’t mention POP3 in this article and for good reason — POP3 support in Rust is virtually non-existent.

For applications that absolutely require async/await for emails, I wouldn’t use Rust just yet. Note that if you just want to slot these crates into an otherwise async application, you can use the threadpool implementation provided by your async runtime. For example, tokio::task::spawn_blocking(...).

Much of the web ecosystem in Rust is already really solid, so I’m confident that the email story will improve over time. Until then, you’re probably better off using another language for email-heavy applications.

LogRocket: Full visibility into production Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — .

Joshua Cooper I'm a full-stack web developer with a passion for well-written and efficient code. On the frontend, I use Typescript with React and on the backend I use Rust or Node.js with Typescript.

Leave a Reply