#smtp-server #mail-server #client-server #smtp #server #server-api #esmtp

letterman

this is a grounds up super fast and light weight smtp client and server library you can integrate it into a rust app to manage email transfer natively

26 releases

0.2.7 Apr 30, 2022
0.2.5 Mar 5, 2022
0.1.0 Dec 27, 2021
0.0.8 Oct 28, 2020

#529 in HTTP server

Download history 159/week @ 2024-07-22 32/week @ 2024-09-23

61 downloads per month

MIT and GPL-3.0 licenses

325KB
7.5K SLoC

letterman - rust

this is a grounds up smtp client and server lib for rust, trust-dns-resolver is used to resolve the mx records of the domains if no records are found smtp is tried on the given domain if not it fails. native-tls is used to support start tls smtp commands works with pretty much all email providers.

domain setup

set spf, dkim and dmarc reocrds on your domain for verifiable email delivery and reception.

Supported SMTP features/extensions

  • RDNS Validation
  • DKIM Validation
  • SPF Lookup
  • 8BITMIME
  • Alternative Body
  • Attachments
  • Base64/Quoted-printable Decoding

testing smtp server

you can test dkim validation, spf lookup and email body validation on dkimvalidator.com

please support the creators of testing servers

Client Api

client api supports pipelinening and can send multiple smtp commands in batch for faster resolution, dkim is required for mail delivery.

use std::time::Instant;
use letterman::client::{read_key,Connection,Email};

#[tokio::main]
async fn main() {

    println!(">>> sending mail async");

    //this is private dkim key for which public key is published at a dkim subdomain as a txt record.
    let key:String;
    match read_key("../secret/private.key".to_string()).await{
        Ok(v)=>{
            key = v;
        },
        Err(e)=>{
            println!("!!! {:?}",e);
            return;
        }
    }

    let mut conn:Connection;
    match Connection::new(
        String::from("localhost"),                      //receiver domain
        String::from("mailcenter.herokuapp.com"),       //server name
        key,                                            //dkim private key
        String::from("dkim"),                           //dkim txt value name
        String::from("silvergram.in"),                  //sender domain
    ){
        Ok(v)=>{conn = v;},
        Err(_)=>{
            return;
        }
    }

    //add emails to this connection
    //emails are parsed before the connection si even started
    //large number of emails in one connection will delay for parsing all emails
    for i in 0..1{
        conn.add(build_mail_new(i.to_string()));
    }

    let hold = Instant::now();

    if true{
        match conn.send().await{
            Ok(_v)=>{
                println!("send successfull  : {:?} {:?}",_v.0,_v.1);
            },
            Err(_e)=>{
                println!("send failed : {:?}",_e);
            }
        }
    }

    println!("finished in  : {:?}",hold.elapsed());

}

fn build_mail_new(tracking_id:String) -> Email{

    let mut email = Email::new();

    email.server_name(String::from("mailcenter.herokuapp.com"));
    email.name(String::from("gzbakku"));
    email.from(String::from("akku@silvergram.in"));
    email.tracking_id(tracking_id);
    email.to(String::from("gzbakku@localhost"));
    //the receivers will receive the message this feature allows cc and bcc smtp functions
    email.receiver(String::from("gzbakku@localhost"));
    email.receiver(String::from("gzbakku1@localhost"));
    email.subject(String::from("hello world"));
    email.body(String::from("first message\r\nsecond message\r\nthird message"));

    //add html body
    if 1 == 1 {
        email.html(
            String::from(
                "<html> <header><title>This is title</title></header> <body> <h1>Hello world</h1> </body> </html>"
            )
        );
    }

    //attach a file
    if 1 == 1 {
        email.attach("d://workstation/expo/rust/letterman/letterman/drink.png".to_string());
    }

    //attach a base64 encoded file
    if 1 == 1 {
        let base64_data = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAJfSURBVDiNpZLdS1MBGMafc87OPtx23M7Opse2hDb8mB+ZLRC70IhBd4GSJAUVYnXVXyBBEHST1xZRmUgXgdHFiC6KwixCyjLDmTKx4cm17Ux2trntnO3YTYrOFYTP3fvwvj8eeF5gn6L+4pPtza6bvJ3xrcWkd/9NrXXaep49vCKP3Dqbomn68Cl/w52BC8emJsYvdpTuasoB0tl88Gngc0TK5GRFUSrP9fou9fZ5tQ8eTY8COAIg+0+AKKaDYxPTbQAUAAeK+U0SKoGVlXXDH29bxM6Bc/B3K4zWWlUtZIuKnC0UCxsESSotddZBzyEXNbcQTi2GxMfxqHC1XAJSpzd1eVs76g0VZtC0dleqaE4G43CYdcLbbgAkALUUoArhpf4ui+Wj286TyXwOObUIANCTFCp1OoRiEVUIL/VvHW8DWI4fZBh2QJLEF7yd/9njO+7UECTYCiMAILGRQWFTxfinKYHlqk8zjG1EkhL3E/G1exqLveqat7njRo3LY1lbDfli+bQa2chg6ZeA+UQcAIEmloOnqgbRXL66qbVziHe6qdXwYv383Ac9yZisgzUujwUATJUspXewtH94CG5vI2aTEmaTSbibGuEfHoLBztJmhqUAwHmwzsIw7GXN+np89OXzsQaG4VoUOZdWOFtbKjBpO2ricLvLDwBwG21IBSaxHFoWgwnxC63VmyQpPqcUisFdNQLAyfrm133tnd3l/uPJzPs3r75/O7HT2/NIZkpbrCUIGEtqTMt5GAhSLd3fA5gRQmd+1LVcD0uCOyRGrVt+RpGlwMLX8+WS7Uu/AV/Q4yOF5rS7AAAAAElFTkSuQmCC".to_string();
        email.attach_base64("drink_1.png".to_string(),base64_data,"image/png".to_string());
    }

    return email;

}

Server Api

server api supports rdns,spf and dkim validation with pipelining extension.


use letterman::server::config::{ServerConfig};
use letterman::server::{init,CheckMail,ProcessMail};
use letterman::json::JsonValue;
use letterman::flume::unbounded as FlumeChannel;
use letterman::flume::Sender as FlumeSender;

#[tokio::main]
async fn main(){

    let config:ServerConfig;
    match ServerConfig::new(
        vec![587,2525],
        String::from("silversender.com"),
        format!("../secret/end.cert"),
        format!("../secret/end.rsa"),
        100_000,
        String::from("../letter_man_que/que/que.akku"),
        5_000_000,
        5,
        String::from("../letter_man_que/email_files/"),
        1,
        false,false,true
    ).await{
        Ok(v)=>{config = v;},
        Err(_)=>{
            return;
        }
    }

    println!(">>> starting server");

    let (check_mail_sender,_check_mail_receiver) = FlumeChannel();
    let (process_mail_sender,_process_mail_receiver) = FlumeChannel();

    match init(
        config,
        check_email,
        process_email,
        check_mail_sender,
        process_mail_sender
    ).await{
        Ok(_)=>{},
        Err(_e)=>{
            println!("!!! server down : {:?}",_e);
        }
    }

    println!("server closed");

}

async fn process_email(_i:ProcessMail,_sender:FlumeSender<JsonValue>) -> Result<(),()>{

    // println!("{:?}",_i.files);

    for i in _i.files.iter(){
        if false {
            match crate::io::delete_file(format!("../letter_man_que/email_files/{}",i)).await{
                Ok(_)=>{
                    // println!("file deleted");
                },
                Err(_)=>{
                    println!("file delete failed");
                }
            }
        }
    }

    Ok(())

}

async fn check_email(_i:CheckMail,_sender:FlumeSender<JsonValue>) -> Result<bool,()>{

    Ok(true)

}

Dependencies

~23–38MB
~721K SLoC