Send Emails with Rust – SMTP and API Methods with Examples

veljkoristic

Veljko Ristic

Posted on December 11, 2023

Send Emails with Rust – SMTP and API Methods with Examples

This article explores the practical steps and considerations for utilizing Rust to manage email sending, from setting up SMTP to leveraging APIs for both sending and testing emails.

How to send emails using Rust and SMTP

The lettre crate is among the most straightforward methods to send emails from Rust via SMTP. The following sections cover different scenarios using lettre crate and they include:

  • Sending a simple plain.txt email
  • Sending an HTML email
  • Sending attachments
  • Sending to multiple recipients

Feel free to copy-paste the scripts below minding your credentials as well as recipient and sender addresses, and SMTP endpoints. Also, note that these are designed for Mailtrap Email Sending SMTP users.

Later in the article, we cover the API method. And here, we’d like to offer some pointers for Mailtrap users.

  • Before you start sending your emails, you need to verify your domain with Mailtrap.
  • Make sure to use the script with TLS handling, since Mailtrap requires STARTTLS.
  • Use only the domain that you set up and verified with Mailtrap. Or, you’ll get the “Unauthorized 401” error.

Send emails using lettre crate

  1. Add ‘lettre’ to the ‘Cargo.toml’ file:
[dependencies]
lettre = "0.10"
lettre_email = "0.9"
Enter fullscreen mode Exit fullscreen mode

Note: the lettre and lettre_emailversions might be updated when you’re reading this article. Click here for the latest versions.

  1. Write the email-sending script:
use lettre::{Message, SmtpTransport, Transport};
use lettre::smtp::authentication::Credentials;

fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    // Define the email
    let email = Message::builder()        .from("Your Name <your.email@example.com>".parse().unwrap())        .reply_to("your.email@example.com".parse().unwrap())        .to("Recipient Name <recipient.email@example.com>".parse().unwrap())        .subject("Rust Email")        .body(String::from("Hello, this is a test email from Rust!"))        .unwrap();
    // Set up the SMTP client    let creds = Credentials::new("Mailtrap_smtp_username".to_string(), "Mailtrap_smtp_password".to_string());
    // Open a remote connection to gmail    let mailer = SmtpTransport::relay("your_mailtrap_Host.io")?        .credentials(creds)        .build();
    // Send the email    match mailer.send(&email) {        Ok(_) => println!("Email sent successfully!"),        Err(e) => eprintln!("Could not send email: {:?}", e),    }
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Important: Replace all the variables with your actual credentials, relay endpoints, and corresponding email addresses.

  1. *TLS handling *

If you’re a Mailtrap user, TLS handling is required. lettre supports ‘None’, ‘Starttls’ and ‘Required’ TLS settings. The TLS settings are specified in the SmtpTransport block, and here’s what the TLS-enabled script might look like.

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

fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    // Build an email message using the builder pattern
    let email = Message::builder()
        // Set the sender's name and email address
        .from("Your Name <your address@gmail.com>".parse().unwrap()) 
        // Set the recipient's name and email address
        .to("Recipient Name <receiver address@gmail.com>".parse().unwrap()) 
        // Set the subject of the email
        .subject("Rust Email") 
        // Set the body content of the email
        .body(String::from("Hello World, this is a test email from Rust!")) 
        .unwrap();

    // Create SMTP client credentials using username and password
    let creds = Credentials::new("mailtrap_username".to_string(), "mailtrap_password".to_string()); 

    // Open a secure connection to the SMTP server using STARTTLS
    let mailer = SmtpTransport::starttls_relay("your_mailtrap_host.io")
        .unwrap()  // Unwrap the Result, panics in case of error
        .credentials(creds)  // Provide the credentials to the transport
        .build();  // Construct the transport

    // Attempt to send the email via the SMTP transport
    match mailer.send(&email) { 
        // If email was sent successfully, print confirmation message
        Ok(_) => println!("Email sent successfully!"), 
        // If there was an error sending the email, print the error
        Err(e) => eprintln!("Could not send email: {:?}", e), 
    }

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Note: your_mailtrap _host will vary depending on your purpose. For example, if you’re using Mailtrap Email Testing, then the Host is sandbox.smtp.mailbox.io

  1. Run your application Use the cargo run command to run your application. Assuming the setup is correct, rust will send the email to specified recipients.

How to send HTML email with Rust?

To send an HTML email, we’ll reuse and modify the lettre script with STARTTLS.

Simply, you need to set the content type of the email body to text/html. This can be done by using the message::SinglePart and message::MultiPart modules to construct the email body properly.

Here’s the modified code:

use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
use lettre::message::{Mailbox, MultiPart, SinglePart};

fn main() ->  std::result::Result<(), Box<dyn std::error::Error>> {

    // Define the HTML content
    let html_content = r#"
        <html>
            <body>
                <h1>Hello!</h1>
                <p>This is a <strong>test email</strong> from Rust!</p>
            </body>
        </html>
    "#;

    let from_email = "Your Name <sender@example.com>".parse::<Mailbox>().unwrap();
    let to_email = "Recipient Name <recipient@example.com>".parse::<Mailbox>().unwrap();

    // Define the email with HTML part
    let email = Message::builder()
        .from(from_email)
        .to(to_email)
        .subject("Rust Email")
        .multipart(
            MultiPart::alternative().singlepart(SinglePart::html(html_content.to_string())),
        )
        .unwrap();

    // Set up the SMTP client credentials
    let creds = Credentials::new("username".to_string(), "password".to_string());

    // Open a remote connection to the SMTP server with STARTTLS
    let mailer = SmtpTransport::starttls_relay("your_mailtrap_host.io")
        .unwrap()
        .credentials(creds)
        .build();

    // Send the email
    match mailer.send(&email) {
        Ok(_) => println!("Email sent successfully!"),
        Err(e) => eprintln!("Could not send email: {:?}", e),
    }

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Notes on code modifications:

  • The html_content variable holds the HTML content of the email.
  • The Message::builder() is used to set up the headers of the email.
  • The multipart() method is used to create a MultiPart email, which can contain both text and HTML parts. In this case, we’re only adding an HTML part using SinglePart::html(html_content.to_string())

How to send an email with attachments in Rust?

Again, we’ll reuse and modify the script above to include attachments.

This code demonstrates how to send an email with attachments using the lettre crate in Rust and the Mailtrap SMTP server.

use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};
use lettre::message::{Mailbox, MultiPart, SinglePart, Attachment, Body};
use std::fs;

fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {


    let image = fs::read("picture.png")?;
    let image_body = Body::new(image);

    let from_email = "Your Name <sender@example.com>".parse::<Mailbox>().unwrap();
    let to_email = "Recipient Name <recipient-email@example.com>".parse::<Mailbox>().unwrap();

    let email = Message::builder()
                    .from(from_email)
                    .to(to_email)
                    .subject("Hello")
                    .multipart(
                        MultiPart::mixed()
                            .multipart(
                                MultiPart::alternative()
                                    .singlepart(SinglePart::plain(String::from("Hello, world! :)")))
                                    .multipart(
                                        MultiPart::related()
                                            .singlepart(SinglePart::html(String::from(
                                                "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
                                            )))
                                            .singlepart(
                                                Attachment::new_inline(String::from("123"))
                                                    .body(image_body, "image/png".parse().unwrap()),
                                            ),
                                    ),
                            )
                            .singlepart(Attachment::new(String::from("example.com")).body(
                                String::from("fn main() { println!(\"Hello, World!\") }"),
                                "text/plain".parse().unwrap(),
                            )),
                    )?;

    let creds = Credentials::new("username".to_string(), "password".to_string());

    // Open a remote connection to the SMTP server with STARTTLS
    let mailer = SmtpTransport::starttls_relay("your_mailtrap_hosting.io").unwrap()
        .credentials(creds)
        .build();

    // Send the email
    match mailer.send(&email) {
        Ok(_) => println!("Email sent successfully!"),
        Err(e) => eprintln!("Could not send email: {:?}", e),
    }

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Notes on code modifications:

  • The code uses the fs::read function from the std library to read the contents of the "picture.png" file into a byte vector. This byte vector is then wrapped in a Body object, which represents the body of the image attachment.
  • In this example, a multipart email is created using MultiPart::mixed(). This multipart email consists of two parts: an alternative multipart (containing a plain text part and a related multipart) and a single-part attachment.
  • The alternative multipart holds both a plain text part (SinglePart::plain) and a related multipart. The related multipart contains an HTML part (SinglePart::html) with an embedded image attachment (Attachment::new_inline).
  • The single-part attachment contains a source code file called "example.com".

When using the script, you need to replace the "picture.png", with the file path you’re using.

Sending email to multiple recipients using Rust

You can send to multiple recipients by chaining .to(),.cc(), and.bcc()methods. Each recipient is added by parsing a string that contains the email address and optionally the name of the recipient.

Check the updated sending script.

// [dependencies]
// lettre="0.10"

use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
use lettre::message::{Mailbox, MultiPart, SinglePart};

fn main() ->  std::result::Result<(), Box<dyn std::error::Error>> {

    // Define the HTML content
    let html_content = r#"
        <html>
            <body>
                <h1>Hello!</h1>
                <p>This is a <strong>test email</strong> from Rust!</p>
            </body>
        </html>
    "#;

    let from_email = "Your Name <sender@example.com>".parse::<Mailbox>().unwrap();
    let to_email = "Recipient Name <receiver@example.com>".parse::<Mailbox>().unwrap();

    // Define the email with HTML part
    let email = Message::builder()
        .from(from_email)
        .to(to_email) 
        // You can also add CC and BCC recipients
        .cc("Recipient CC <recipient.cc@example.com>".parse::<Mailbox>().unwrap())
        .bcc("Recipient BCC <recipient.bcc@example.com>".parse::<Mailbox>().unwrap())
        .subject("Rust Email")
        .multipart(
            MultiPart::alternative().singlepart(SinglePart::html(html_content.to_string())),
        )
        .unwrap();

    // Set up the SMTP client credentials
    let creds = Credentials::new("username".to_string(), "password".to_string());

    // Open a remote connection to the SMTP server with STARTTLS
    let mailer = SmtpTransport::starttls_relay("your_mailtrap_hosting.io")
        .unwrap()
        .credentials(creds)
        .build();

    // Send the email
    match mailer.send(&email) {
        Ok(_) => println!("Email sent successfully!"),
        Err(e) => eprintln!("Could not send email: {:?}", e),
    }

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

How to send emails using Rust and Mailtrap API?

To send emails using the Mailtrap Email Sending API in Rust, use an HTTP client library to make a POST request to the API endpoint.

The commonly used HTTP client library in Rust is reqwest, and I chose it because it’s among the easiest to implement.

Now, start by adding the necessary dependencies. Add reqwest and tokio to your Cargo.toml because reqwest is an async library:

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
serde_json = "1.0"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
Enter fullscreen mode Exit fullscreen mode

Here’s an exemplary script to send emails with Rust and Mailtrap API.

use reqwest;
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let api_url = "https://send.api.mailtrap.io/api/send";
    let api_key = "your_api_key";
    let email_payload = json!({
        "from": {"email" : "your_verified_domain"},
        "to": [{"email": "receiver@example.com"}],
        "subject": "Test Email",
        "text": "This is a test email using Rust and Mailtrap API!",
    });

    let client = reqwest::Client::new();
    let response = client
        .post(api_url)
        .header("Content-Type", "application/json")
        .header("Api-Token", api_key)
        .body(email_payload.to_string()) // Serialize the JSON payload to a string
        .send()
        .await?;

    if response.status().is_success() {
        println!("Email sent successfully!");
    } else {
        println!("Failed to send email. Status: {:?}", response.status());

        // Print the response body for additional information
        let body = response.text().await?;
        println!("Response body: {}", body);
    }

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Notes on the Mailtrap API method:

  • The email_payload variable contains a JSON object that represents the email data.
  • A client instance is created using reqwest::Client::new(). This client will be used to make the POST request to the Mailtrap API.
  • The client.post(api_url) method creates a POST request to the specified endpoint URL. The headers are set using the .header() method, including the Content-Type header as "application/json" and the API-Token header as the Mailtrap API key.
  • The email payload is serialized to a string using email_payload.to_string() and set as the request body using the .body() method.
  • The send()method sends the request and returns a future that resolves to a Result<Response, reqwest::Error> The await keyword is used to await the completion of the request and get the response.
  • The response status is checked using the .status() method. If the status indicates success (2xx) a success message is printed. Otherwise, an error message is printed along with the response status.
  • Any errors that occur during the request or response handling are propagated by returning a Result<(), reqwest::Error> from the main() function. Error handling in Rust typically involves propagating the error upwards in the call stack by returning it from the function.

Before running this code, ensure that you have the reqwest and tokio crates added to your Cargo.toml file with the appropriate versions and features enabled.

Send emails using sendmail crate

The sendmail crate sends emails by interfacing with the sendmail command available on many Unix-like systems. This means that the system where your Rust application is running must have a sendmail-compatible program installed and properly configured.

The quick tutorial below assumes you have a proper sendmail-compatible program installed and configured. Otherwise, the method won’t work.

If you need a tutorial on setting up sendmail on Ubuntu, check the one in the link.

  1. Add sendmail command to the Cargo.toml:
[dependencies]
sendmail = "2.0"
Enter fullscreen mode Exit fullscreen mode
  1. Use the script below to send your email:
extern crate sendmail;
use sendmail::email;

fn main() {

    // Configure email body and header
    // Send the email
    match email::send(
        // From Address
        "sender@example.com",
        // To Address
        &["receiver@example.com"],
        // Subject
        "Subject - Hello World!",
        // Body
        "<html><body><h1>I am the body. Hello Wolrd!<br/><br/>And I accept html.</h1></body></html>"
    ) {
        Ok(_) => println!("Email sent successfully!"),
        Err(e) => eprintln!("Could not send email: {:?}", e),
    }
}
Enter fullscreen mode Exit fullscreen mode

Keynotes on using the sendmail crate:

  • sendmail(&email) is used to send the email. This function takes the email object and sends it using the system’s sendmail command.
  • As the method relies on the system-level sendmail command, it’s less “portable” than the lettre which can work across different platforms without system-specific tools.

Rust email testing: reasons and how-tos

There are five reasons to consider email testing with Rust:

  1. Verification of Logic: Ensure that your email-sending logic works as expected under various conditions.
  2. Content Accuracy: Make sure that the content of the emails, including subject, body (both text and HTML), and attachments, is correct.
  3. Error Handling: Test how your application handles failures, such as network issues or incorrect credentials.
  4. Performance: Understand how your email sending performs under load. This is especially important if you’re sending large volumes of email.
  5. Security: Ensure that your email-sending process is secure and that sensitive information is handled correctly.

Also, there are three types of tests you can run: integration, unit, and end-to-end testing. Check the details below.

Integration testing with Mailtrap Testing SMTP

Use Mailtrap Email Testing for integration tests. It gives you a safe environment to inspect and debug your emails without the risk of spamming your recipients.

Here’s an exemplary code for Mailtrap users.

Note: Run this application with ‘cargo test’ command to see the result.

// Example of integration test with a fake SMTP server
fn send_test_email() -> Result<(), Box<dyn std::error::Error>> {
        // Build the email
    let email = EmailBuilder::new()
        // Addresses can be specified by the tuple (email, alias)
        .to(("recipient.email@example.com", "Recipient Name"))
        .from(("your.email@example.com", "Your Name"))
        .subject("Rust Email")
        .body("Hello, this is a test email from Rust!")
        .build()?;

    // Send the email
    match sendmail(&email) {
        Ok(_) => println!("Email sent successfully!"),
        Err(e) => eprintln!("Could not send email: {:?}", e),
    }

    Ok(())

}

#[test]
fn test_email_integration() {
    // Start a fake SMTP server before running the test
    // ...

    let result = send_test_email();
    assert!(result.is_ok());

    // Check the fake SMTP server for the sent email
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Important Note: The code above doesn’t contain an exemplary email template.

As mentioned this integration test is for Mailtrap users, if you’re not a user, you can sign up here. We offer a free plan where you can check things out at a rate of 100 emails a month with a throughput of 5 emails per 10 seconds.

Here’s a quick overview of what you get with Mailtrap Email Testing:

Integration testing with Mailtrap Testing API

We’ll follow a similar approach used for Mailtrap Email Sending API. Only, this time the endpoints and details are specific to the testing API.

Again, you need the reqwest and tokio crates in the Cargo.toml file to may asynchronous HTTP requests.

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
serde_json = "1.0"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
Enter fullscreen mode Exit fullscreen mode

And here’s a exemplary API testing script.

use reqwest;
use serde_json::json;
use tokio;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api_url = "https://sandbox.api.mailtrap.io/api/send/inbox_id"; // Replace 'inbox_id' with your actual Mailtrap inbox ID
    let api_token = "api_token"; // Replace with your actual Mailtrap API token

    let client = reqwest::Client::new();

    let payload = json!({
        "to": [
            {
                "email": "receiver@gmail.com",
                "name": "receiver name"
            }
        ],
        "from": {
            "email": "your.domain.link",
            "name": "Example Sales Team"
        },
        "subject": "Your Example Order Confirmation",
        "text": "Congratulations on your order no. 1234",
        "category": "API Test"
    });

    let response = client.post(api_url)
        .header("Content-Type", "application/json")
        .header("Accept", "application/json")
        .header("Api-Token", api_token)
        .json(&payload)
        .send()
        .await?;

    if response.status().is_success() {
        println!("Email sent successfully!");
    } else {
        println!("Failed to send email. Status: {:?}", response.status());

        // Print the response body for additional information
        let body = response.text().await?;
        println!("Response body: {}", body);
    }


    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Keynotes about the API testing script:

  • Replace "inbox_id" with your actual Mailtrap inbox ID (integer).
  • Replace "your_api_token" with your actual Mailtrap API token.
  • The payload is constructed using serde_json::json! macro to create a JSON object.
  • The reqwest::Client is used to make a POST request to the Mailtrap Testing API endpoint. The response is checked for success, and the response body is printed out.

Lastly, you should run the code within an async environment since reqwest is an asynchronous library. The #[tokio::main] attribute macro is used to set up an asynchronous runtime for the main function.

Unit testing

Unit testing allows you to mock the components that send emails. Rust libraries designed for that are mockall or mockito, and they create mock objects in the tests.

The below is a simplified version of how you might mock an email sender.

use mockall::predicate::*;
use mockall::Sequence;
use mockall::automock;

#[automock]
trait EmailSender {
    fn send_email(&self, recipient: &str, subject: &str, body: &str) -> Result<(), String>;
}

struct MyComponent<T: EmailSender> {
    email_sender: T,
}

impl<T: EmailSender> MyComponent<T> {
    pub fn new(email_sender: T) -> Self {
        MyComponent { email_sender }
    }

    pub fn do_something(&self) -> Result<(), String> {
        // Code that uses the email sender component
        let recipient = "recipeint@example.com";
        let subject = "Test Subject";
        let body = "Test Body";

        self.email_sender.send_email(recipient, subject, body)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_do_something() {
        let mut email_sender = MockEmailSender::new();

        let recipient = "recipient@example.com";
        let subject = "Test Subject";
        let body = "Test Body";

        // Set up expectations for the mock object
        email_sender.expect_send_email()
            .once()
            .withf(move |r, s, b| r == recipient && s == subject && b == body)
            .returning(|_, _, _| Ok(()));

        let my_component = MyComponent::new(email_sender);
        let result = my_component.do_something();

        assert!(result.is_ok());
    }
}
Enter fullscreen mode Exit fullscreen mode

Key notes

  • The #[automock] attribute is used before the EmailSender trait definition. This attribute generates an implementation of the trait with all its methods implemented as mock methods. It allows you to set expectations on the behavior of the mock object during testing.
  • Using the expect_send_email method on the email_sender mock object, you set expectations for the send_email method of the mock object.
  • In the exemplary script, the method should be called once (once()) and with the expected recipient, subject, and body values. We use the withf method to define a closure that checks if the passed arguments match the expected ones.
  • The returning method is used to define the return value of the send_email method, which in this case is Ok(())
  • I created an instance of MyComponent by passing the mocked email_sender object. Then, we call the do_something method on the instance and store the result in the result variable.

End-to-end testing

To run end-to-end tests, you might actually send production (not sandbox) emails to a controlled set of email addresses. Then, you could use a specific API or service to confirm that the emails were received and contain the right content.

There’s no exemplary code here as it depends on the type of emails you want to test, what services you’re using, and your overall approach. So, it’s a topic on its own and won’t be covered in detail here.

However, if you run integration testing, end-to-end might not be necessary.

Trust Rust

Rust’s versatility extends to email communication, providing developers with powerful tools to send and test emails effectively. Whether through SMTP or API integration, the language’s safety features and performance make it an excellent choice for managing email-related tasks in modern applications.

Thank you for reading this article! Follow Mailtrap blog and discover more about sending emails using Rust and SMTP

💖 💪 🙅 🚩
veljkoristic
Veljko Ristic

Posted on December 11, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

December Surely Looks Busy!
opensource December Surely Looks Busy!

November 29, 2024

December Surely Looks Busy!
opensource December Surely Looks Busy!

November 29, 2024

Daemons on macOS with Rust
undefined Daemons on macOS with Rust

November 29, 2024