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.
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("[email protected]") .from("[email protected]") .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(("[email protected]", "Alice Smith")) .from(("[email protected]", "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("[email protected]") .from("[email protected]") .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:
use lettre_email::mime; use lettre_email::EmailBuilder; use std::path::Path; fn main() { let email = EmailBuilder::new() .to("[email protected]") .from("[email protected]") .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
.
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("[email protected]") .from("[email protected]") .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("[email protected]") .from("[email protected]") .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.
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("[email protected]".parse().unwrap()) .from("[email protected]".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.
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("[email protected]", "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.
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.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the 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 and mobile apps, recording literally everything that happens on your Rust application. 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 — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]