#smtp #server #client #smtps #esmtp

letterman

this is a light weight grounds up email lib for rust

11 releases

0.1.1 Jan 11, 2022
0.1.0 Dec 27, 2021
0.0.9 Dec 25, 2021
0.0.8 Oct 28, 2020

#44 in HTTP server

Download history 15/week @ 2021-09-27 8/week @ 2021-10-11 8/week @ 2021-11-08 2/week @ 2021-11-22 8/week @ 2021-11-29 9/week @ 2021-12-06 14/week @ 2021-12-20 22/week @ 2021-12-27 4/week @ 2022-01-03 21/week @ 2022-01-10

61 downloads per month

Custom license and GPL-3.0 licenses

1.5MB
5.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

~22MB
~499K SLoC

)a