I’m building a little web server with Rust and as part of it, I’m going to need to send some e-mails to my users. I found a few services that offer an e-mail API with a free tier, and decided to go with Brevo.

Authenticating our domain

In order for our e-mails to reach our users’ inboxes, we need to correctly configure our DKIM and DMARC records so they can be used by Brevo.

We can’t authenticate e-mails from free email providers like Gmail. So, before we can authenticate our domain, we need to own a domain. I’m going to use my blog’s domain (ncona.com).

Once we have a domain, we need to add it to our domains list. Click one of the Add a domain buttons to do that:

Click add a domain button

A pop-up will open. We just need to fill our domain name and click Add a domain:

Add domain pop up

A new pop-up will ask us if we want to authenticate ourselves, or someone else will do it. We’ll choose to do it ourselves:

Choose authentication method

In the next screen, we will be presented with a few DNS records we will need to add to our domain’s DNS provider:

DNS records

After setting those DNS records, we can click Authenticate this email domain to let Brevo do the authentication.

Once the domain is authenticated, we will be able to see it in our domains list:

Domain authenticated

Generating an API key

In order to generate an API key, we just need to go to the API keys page and click Generate a new API key:

Generate API key

A pop-up will open asking for a name for our API key:

Name API key

Then our key will be presented to us:

Your API key

It’s important to keep this key safe, as it allows the holder to send e-mails from our Brevo account.

Handling configurations

Since our Brevo key is a secret, we need a way for our system to access it, without embedding it in the code. To achieve this, we can use the config crate.

This crate allows us to easily read configurations from environment variables so we can use them in our code.

We’ll start by defining some structs where we’ll store our configurations:

1
2
3
4
5
6
7
8
9
#[derive(Debug, Deserialize)]
pub struct Mail {
    pub api_key: String,
}

#[derive(Debug, Deserialize)]
pub struct Settings {
    pub mail: Mail,
}

We can then use the config library to load our environment variables into these structs. A good place to do this is in the new function of the root struct:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
impl Settings {
    pub fn new() -> Self {
        let s = match Config::builder()
            .add_source(Environment::with_prefix("APP").separator("__"))
            .build()
        {
            Ok(s) => s,
            Err(err) => panic!("Couldn't build configuration. Error: {}", err),
        };

        match s.try_deserialize() {
            Ok(s) => s,
            Err(err) => panic!("Couldn't deserialize configuration. Error: {}", err),
        }
    }
}

Notice how we define APP as prefix, and __ as separator. This means that to set the api_key field, we need this environment variable:

1
APP__MAIL__API_KEY

Sending an e-mail

Now that our API key is available in our code, we can use reqwest to send e-mails. In this example, we’re going to use the blocking client, as it’s the simplest, but reqwest also has an async client that is more fitting for production use.

According to Brevo’s API, the body of the request needs to be something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let body_str = r#"{
    "sender": {
        "name": "Sender name",
        "email": "sender@yourdomain.com"
    },
    "to": [
        {
            "name": "Recipient name",
            "email": "recipient@email.com"
        }}
    ],
    "subject": "Testing brevo",
    "htmlContent": "<html><body>Hello, world!</body></html>"
}"#;

To send the request, we can use this code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let body: serde_json::Value = serde_json::from_str(&body_str).expect("Invalid JSON");

let s = Settings::new();

let client = reqwest::blocking::Client::new();
match client.post("https://api.brevo.com/v3/smtp/email")
        .header("accept", "application/json")
        .header("content-type", "application/json")
        .header("api-key", s.mail.api_key)
        .json(&body)
        .send() {
    Ok(res) => {
        println!("Status: {}", res.status());
        match res.text() {
            Ok(rt) => println!("Response: {}", rt),
            Err(err) => panic!("Error: {}", err),
        }
    },
    Err(err) => panic!("Error sending the request. Error: {}", err),
}

Note how we use s.mail.api_key to access our API key.

Conclusion

E-mail API providers make it very easy to send e-mails to our users. The most important step is to allow the provider to send those e-mails from our domain by setting the correct DNS records.

As usual, you can find a working version of the code in this article in my examples’ repo.

[ rust  programming  automation  ]
Asynchronous Programming with Tokio
Programming Concurrency in Rust
Smart Pointers in Rust
Rust References Lifetimes
Traits, Rust's Interfaces