Skip to content

Actix Web

Guide for applications built with Actix Web (Rust). Mailexam connects as an SMTP server; sending uses the lettre crate with an async transport on Tokio (Actix Web 4 runs on Tokio).

What you need

  • A Mailexam account and a project with SMTP credentials.
  • Rust 1.70+ and a project on Actix Web 4.

Copy from the welcome email (or dashboard) for your project:

  • YOUR_LOGIN — SMTP login (for example, xxxxx);
  • YOUR_PASSWORD — SMTP password (a unique pair with the login);
  • host — YOUR_LOGIN.mailexam.io (matches the login; see code below).

1. Dependencies

In Cargo.toml:

[dependencies]
actix-web = "4"
serde = { version = "1", features = ["derive"] }
lettre = { version = "0.11", default-features = false, features = [
    "builder",
    "smtp-transport",
    "tokio1-rustls-tls",
] }

To load settings from the environment (optional):

dotenvy = "0.15"

2. Environment variables

.env file in the project root (do not commit passwords to git):

MAILEXAM_LOGIN=YOUR_LOGIN
MAILEXAM_PASSWORD=YOUR_PASSWORD
MAILEXAM_PORT=587
MAIL_FROM=noreply@example.test

The host is built from the login: {MAILEXAM_LOGIN}.mailexam.io.

Sender address

MAIL_FROM can be any test address — the message goes to Mailexam, not to a real recipient.

Alternative ports

MAILEXAM_PORT=587

Use AsyncSmtpTransport::starttls_relay (see below).

MAILEXAM_PORT=2525
MAILEXAM_PORT=25

Instead of starttls_relay, use AsyncSmtpTransport::builder(...).port(25) without mandatory TLS — only if that is acceptable on your network.

3. Mail sending module

The same module as for Axum: MailConfig::from_env() and send_test via lettre.

// src/mail.rs
use lettre::message::header::ContentType;
use lettre::transport::smtp::authentication::Credentials;
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};

#[derive(Clone)]
pub struct MailConfig {
    pub host: String,
    pub port: u16,
    pub username: String,
    pub password: String,
    pub from: String,
}

impl MailConfig {
    pub fn from_env() -> Self {
        let login = std::env::var("MAILEXAM_LOGIN").expect("MAILEXAM_LOGIN");
        Self {
            host: format!("{login}.mailexam.io"),
            port: std::env::var("MAILEXAM_PORT")
                .unwrap_or_else(|_| "587".into())
                .parse()
                .expect("MAILEXAM_PORT"),
            username: login,
            password: std::env::var("MAILEXAM_PASSWORD").expect("MAILEXAM_PASSWORD"),
            from: std::env::var("MAIL_FROM")
                .unwrap_or_else(|_| "noreply@example.test".into()),
        }
    }
}

pub async fn send_test(
    config: &MailConfig,
    to: &str,
    subject: &str,
    body: &str,
) -> Result<(), lettre::transport::smtp::Error> {
    let email = Message::builder()
        .from(config.from.parse().expect("MAIL_FROM"))
        .to(to.parse().expect("recipient"))
        .subject(subject)
        .header(ContentType::TEXT_PLAIN)
        .body(body.to_string())
        .expect("message");

    let creds = Credentials::new(config.username.clone(), config.password.clone());

    let mailer = AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&config.host)?
        .port(config.port)
        .credentials(creds)
        .build();

    mailer.send(email).await
}

4. Actix Web handler

// src/main.rs
mod mail;

use actix_web::{post, web, App, HttpResponse, HttpServer, Responder};
use mail::MailConfig;
use serde::Deserialize;

#[derive(Deserialize)]
struct SendRequest {
    to: String,
    subject: Option<String>,
    body: Option<String>,
}

#[post("/mail/test")]
async fn send_mail(
    config: web::Data<MailConfig>,
    payload: web::Json<SendRequest>,
) -> impl Responder {
    mail::send_test(
        &config,
        &payload.to,
        payload.subject.as_deref().unwrap_or("Actix + Mailexam"),
        payload.body.as_deref().unwrap_or("Mailexam test from Actix"),
    )
    .await
    .expect("smtp send");

    HttpResponse::Ok().body("ok")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    dotenvy::dotenv().ok();

    let config = web::Data::new(MailConfig::from_env());

    HttpServer::new(move || {
        App::new()
            .app_data(config.clone())
            .service(send_mail)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

Start and verify:

cargo run
curl -X POST http://127.0.0.1:8080/mail/test \
  -H 'Content-Type: application/json' \
  -d '{"to":"user@example.test","subject":"Test","body":"Hello"}'

The message will appear in the Mailexam dashboard → your project → inbox.

5. Local development and CI

Environment Recommendation
local .env with a personal Mailexam project
CI Secrets MAILEXAM_LOGIN, MAILEXAM_PASSWORD in GitLab CI/CD Variables

Example for .gitlab-ci.yml:

variables:
  MAILEXAM_LOGIN: $MAILEXAM_LOGIN
  MAILEXAM_PASSWORD: $MAILEXAM_PASSWORD
  MAILEXAM_PORT: "587"
  MAIL_FROM: "noreply@example.test"

After an integration test with message sending, verify delivery via the Mailexam API.

6. Common issues

TLS or connection error

  • Host must be {login}.mailexam.io, where {login} is the same value as MAILEXAM_LOGIN from the email.
  • Login and password are a pair from the email; do not combine credentials from different projects.
  • For port 587 use starttls_relay, not SMTPS on 465.

Message not in the dashboard

  • Make sure you are viewing the inbox of the same Mailexam project.
  • Check Actix worker logs: SMTP errors appear when send_test fails.

Building lettre

  • For TLS you need the tokio1-rustls-tls feature (or tokio1-native-tls if needed).

Port in use

  • By default the server listens on 8080; change the address in .bind(...) if there is a conflict.

See also