rs-smtp

An ESMTP server library written in Rust.

Features

Usage

Add this to your Cargo.toml:

```toml [dependencies] rs-smtp = "1"

anyhow = "1.0" tokio = { version = "1.26.0", features = ["full"] } async-trait = "0.1.67" ```

Example

Simple Example

```rust use anyhow::Result; use asynctrait::asynctrait; use rs_smtp::sasl; use tokio::io::{AsyncReadExt, AsyncRead};

use rssmtp::backend::{Backend, Session, MailOptions}; use rssmtp::conn::Conn; use rs_smtp::server::Server;

struct MyBackend;

struct MySession;

impl Backend for MyBackend { type S = MySession;

fn new_session(&self, _c: &mut Conn<Self>) -> Result<MySession> {
    Ok(MySession)
}

}

[async_trait]

impl Session for MySession { fn authenticators(&mut self) -> Vec> { vec!() }

async fn mail(&mut self, from: &str, _: &MailOptions) -> Result<()> {
    println!("mail from: {}", from);
    Ok(())
}

async fn rcpt(&mut self, to: &str) -> Result<()> {
    println!("rcpt to: {}", to);
    Ok(())
}

async fn data<R: AsyncRead + Send + Unpin>(&mut self, mut r: R) -> Result<()> {
    // print whole message
    let mut mail = String::new();
    r.read_to_string(&mut mail).await?;
    println!("data: {}", mail);

    Ok(())
}

fn reset(&mut self) {}

fn logout(&mut self) -> Result<()> {
    Ok(())
}

}

[tokio::main]

async fn main() -> Result<()> { let be = MyBackend;

let mut s = Server::new(be);

s.addr = "127.0.0.1:2525".to_string();
s.domain = "localhost".to_string();
s.read_timeout = std::time::Duration::from_secs(10);
s.write_timeout = std::time::Duration::from_secs(10);
s.max_message_bytes = 10 * 1024 * 1024;
s.max_recipients = 50;
s.max_line_length = 1000;
s.allow_insecure_auth = false;

println!("Starting server on {}", s.addr);
match s.listen_and_serve().await {
    Ok(_) => println!("Server stopped"),
    Err(e) => println!("Server error: {}", e),
}

Ok(())

} ```

You can use the server manually with telnet:

bash telnet localhost 2525 EHLO localhost MAIL FROM:<root@nsa.gov> RCPT TO:<root@gchq.gov.uk> DATA Hey <3 .

Outgoing Mail Server Example With Authentication and STARTTLS

```rust use anyhow::Result; use asynctrait::asynctrait; use mailsend::SmtpClientBuilder; use mailsend::mailauth::common::crypto::{RsaKey, Sha256}; use mailsend::mailauth::dkim::DkimSigner; use rssmtp::sasl; use rustlspemfile::{certs, pkcs8privatekeys}; use tokio::io::{self, AsyncReadExt, AsyncRead}; use tokiorustls::rustls::{self, Certificate, PrivateKey}; use tokio_rustls::TlsAcceptor;

use std::borrow::Cow; use trustdnsresolver::AsyncResolver; use trustdnsresolver::config::*;

use mail_send::smtp::message::{ Address, Message };

use std::fs::File; use std::io::BufReader; use std::sync::Arc;

use rssmtp::backend::{Backend, Session, MailOptions}; use rssmtp::conn::Conn; use rssmtp::sasl::plain::{PlainServer, PlainAuthenticator}; use rssmtp::server::Server;

struct MyBackend;

struct MySession { to: Vec, }

impl Backend for MyBackend { type S = MySession;

fn new_session(&self, _c: &mut Conn<Self>) -> Result<MySession> {
    Ok(MySession {
        to: Vec::new(),
    })
}

}

struct MyPlainAuthenticator;

[async_trait]

impl PlainAuthenticator for MyPlainAuthenticator { async fn authenticate(&mut self, _identity: &str, username: &str, password: &str) -> Result<()> { if username == "nick@dunef.io" && password == "test" { Ok(()) } else { Err(anyhow::anyhow!("invalid username or password")) } } }

[async_trait]

impl Session for MySession { fn authenticators(&mut self) -> Vec> { vec!( Box::new(PlainServer::new(MyPlainAuthenticator)) ) }

async fn mail(&mut self, from: &str, _: &MailOptions) -> Result<()> {
    println!("mail from: {}", from);
    Ok(())
}

async fn rcpt(&mut self, to: &str) -> Result<()> {
    println!("rcpt to: {}", to);
    self.to.push(to.to_string());
    Ok(())
}

async fn data<R: AsyncRead + Send + Unpin>(&mut self, mut r: R) -> Result<()> {
    // print whole message
    let mut mail = vec![];
    r.read_to_end(&mut mail).await?;

    let resolver = AsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default())?;

    let pk_rsa = RsaKey::<Sha256>::from_rsa_pem(DKIM_KEY).unwrap();
    let signer = DkimSigner::from_key(pk_rsa)
        .domain("dunef.io")
        .selector("dkim_selector")
        .headers(["From", "To", "Subject"])
        .expiration(60 * 60 * 7);

    for to in self.to.iter() {

        println!("Sending email to {}", to);

        let mx_response = resolver.mx_lookup(to.split("@").collect::<Vec<&str>>()[1]).await?;

        for mx in mx_response.iter() {

            println!("MX: {}", mx.exchange());

            let builder = SmtpClientBuilder::new(mx.exchange().to_utf8(), 25)
                .helo_host("dunef.io".to_string())
                .implicit_tls(false)
                .connect()
                .await;

            if builder.is_err() {
                println!("Failed to connect to {}", mx.exchange());
                continue;
            }

            match builder.unwrap()
                .send_signed(
                    Message::new(
                        Address::from("nick@dunef.io"),
                        vec![Address::from(to.clone())],
                        Cow::from(&mail),
                    ),
                    &signer
                )
                .await {
                    Ok(_) => {
                        println!("Email sent successfully");
                        break;
                    },
                    Err(e) => {
                        println!("Failed to send email: {}", e);
                        continue;
                    }
                }
        }
    }

    Ok(())
}

fn reset(&mut self) {}

fn logout(&mut self) -> Result<()> {
    Ok(())
}

}

[tokio::main]

async fn main() -> Result<()> { let be = MyBackend;

let mut s = Server::new(be);

s.addr = "127.0.0.1:587".to_string();
s.domain = "dunef.io".to_string();
s.read_timeout = std::time::Duration::from_secs(10);
s.write_timeout = std::time::Duration::from_secs(10);
s.max_message_bytes = 10 * 1024 * 1024;
s.max_recipients = 50;
s.max_line_length = 1000;
s.allow_insecure_auth = false;

let certs = load_certs("/etc/letsencrypt/live/dunef.io/fullchain.pem")?;
let mut keys = load_keys("/etc/letsencrypt/live/dunef.io/privkey.pem")?;

let config = rustls::ServerConfig::builder()
    .with_safe_defaults()
    .with_no_client_auth()
    .with_single_cert(certs, keys.remove(0))
    .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
let acceptor = TlsAcceptor::from(Arc::new(config));

s.tls_acceptor = Some(acceptor);

println!("Starting server on {}", s.addr);
match s.listen_and_serve().await {
    Ok(_) => println!("Server stopped"),
    Err(e) => println!("Server error: {}", e),
}

Ok(())

}

fn loadcerts(path: &str) -> io::Result> { certs(&mut BufReader::new(File::open(path)?)) .maperr(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid cert")) .map(|mut certs| certs.drain(..).map(Certificate).collect()) }

fn loadkeys(path: &str) -> io::Result> { pkcs8privatekeys(&mut BufReader::new(File::open(path)?)) .maperr(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key")) .map(|mut keys| keys.drain(..).map(PrivateKey).collect()) }

const DKIM_KEY: &str = "-----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEAwbIlk19eVTa8RayErwcr0PWkH+IDZSddxVw2AKpujKJN642e PiwMChgj5LRwFdWknT97E9YykVk46vNPdKTjBe1POuUrl2wcm46Hv0easuqiyjRC WswNIN+avvG2krgMgf3/2T12lyJ2v4wcn34q9vqN+dkLfMz9WWbKEeKHHq80SFyL PHA6ieTNToXgHgP+Qr5lbL6gh8fjTiUtpIVvHt8N29TIBTpVi3StE+2cu/RCSfcZ 6Wu6tJYzRxlTIKWFxU5Fh+5YL0gue3M7+1e5FyeQHChyRljpDWWywYPGrlN4Thon KV2nwwV1v3B/1dBEKuXuo3lNPOrkwkc4rgsPvQIDAQABAoIBAQCoIvUdNWbUf5v0 uym+KXJuhByBFJcv4nkyjbXO5CLsbyNGevtHKsMUrBnUOJEnUvn/ChDTilcA9rtC sAxjy5HKHlJtZGtvmQhIO/Q4JXbzIlxHPA/xczleNNvGLln2iE9LM+o4cHMWBHOi GITsKgAvvhUqMa8YGXU+esyjs8jo51fyo/XdqUE3iLjm3+d2ZZnC8vjpKdVOIfGd C/8TLXDFkj1MawRVEBgV06Mqnjxg9pPEwcuxYhTWXxwKXjLCoJ2NmXx4k5+6vsDC 5mqnb0/aOIMX6ZCmkTWS6Sn9HuvVJ3Qvmb3FVh8gk4WY2xS7//emS9hXsrj9qZez AJxP+Qp9AoGBAO5oK523lRR4yqRqHxEME6aRxKzG74zWVMYKS0SpY8lU7YQDe8AW xwN4c4FPnxIplm/XNT/EUA8bQ9lZBY8rasTB6nYegujwbITk/QIT7CHdUxmZ4keW 3K18Yn3RZjYJrxAlE5JB756kISsz+rauguYMDi9enLA8hbXd2NBicIbLAoGBAM/9 UxEkgY2+/Eltz0idDSDWpD/DiXgNV8YE7fmZQkE9FZDjnmwGPpfyKjX+gN1dvmk8 qjBsJ6u5DkzyA30IyzUVxaz6Qq0/z1SHzNABoN1leB4ASxruMFSoaeWkb17dKAQ/ KDC/U447VVI5fDatrqsFB1F0CO470KQukTaiouqXAoGBAObKGycD+CqkzS7auJZV HZTLWhx0PKQXPFu2zWR7omjdeUyp3pt2sVOvwAk3XeNENSixqg+/6EyndUgrwJD3 U9WDb4jHQq1jSXpg/niLdrTVv8Nxz7bD2X9sgSARnSPEvh8f9VFJ2UC23JEpMZS1 XWx70SOUMJT/EeWcDG62TP5/AoGAOsnDnOjQpZwB+09Kc5/QgiOpMUy3onNDB/mE ujQTghP8gIOV17q8Hn6YZ8KT8f35QA2hnSY04FjiLeWKDuFZbpvEz+u8xPNwSthH j9OmAG4Z0YELuYTxrDweEoaz5ABmuyyO05iAqYcjyqXs8heNc1FsjB1cGNpXUtDG wsadfekCgYEAlVvU+StUkNU1bHaKUXc/pa1jCIWkd2a9Hi4/Wibd8YzWF3TZ2hdi kSWZOLGi8nCYqNDu+oN0t5ItS9bFvuy+cUkuimK0GgMksAwumXSIRx064oguXKGl FIa62JUQcV2v55xrtLYX+7emRwzq6zUPU9pkn9rqgK44xU8HDvcKgSM= -----END RSA PRIVATE KEY----- "; ```

License

MIT