neo-email 0.1.0

The modern way to build emails services with Rust
Documentation
# Full Example

A full example with TLS and Logger

```rust
use std::fs::File;
use std::io::{BufReader, Read};
use std::net::SocketAddr;
use std::sync::Arc;

use neo_email::connection::SMTPConnection;
use neo_email::controllers::on_auth::OnAuthController;
use neo_email::controllers::on_email::OnEmailController;
use neo_email::headers::EmailHeaders;
use neo_email::mail::Mail;
use neo_email::message::Message;
use neo_email::server::SMTPServer;

use neo_email::status_code::StatusCodes;
use tokio::sync::Mutex;
use tokio_native_tls::native_tls::{Identity, TlsAcceptor};
use tokio_native_tls::TlsAcceptor as TokioTlsAcceptor;

use colored::*;
use fern::Dispatch;

#[derive(Debug, Clone, Default)]
pub struct ConnectionState {
    pub authenticated: bool,
}

#[tokio::main]
async fn main() {
    set_logger().unwrap();
    log::debug!("Starting server");

    let addr = SocketAddr::from(([127, 0, 0, 1], 2526));

    // Load the certificate
    // check examples/certificates/README.md for more information
    let cert_path = "server.p12"; // Adjust this path to your certificate file
    let cert_password = "your_password"; // Adjust this to your certificate password

    let file = File::open(cert_path).expect("Cannot open certificate file");
    let mut reader = BufReader::new(file);
    let mut identity_data = Vec::new();
    reader.read_to_end(&mut identity_data).expect("Cannot read certificate file");

    let identity = Identity::from_pkcs12(&identity_data, cert_password).expect("Cannot create identity from certificate");

    // Create the native_tls acceptor
    let tls_acceptor = TlsAcceptor::builder(identity).build().expect("Cannot build TLS acceptor");

    // Convert the native_tls acceptor to a tokio-native-tls acceptor
    let tokio_tls_acceptor = TokioTlsAcceptor::from(tls_acceptor);

    // Create the server
    SMTPServer::<ConnectionState>::new()
        // Set the number of workers to 1
        .workers(1)
        // Set the TLS acceptor
        .set_tls_acceptor(tokio_tls_acceptor)
        // Set an controller to dispatch when an authentication is received
        .on_auth(OnAuthController::new(on_auth))
        // Set an controller to dispatch when an email is received
        .on_email(OnEmailController::new(on_email))
        // Bind the server to the address
        .bind(addr)
        .await
        .unwrap()
        // Run the server
        .run()
        .await;
}

// This function is called when an authentication is received
// What is data?
// This send the client: `AUTH PLAIN AHlvdXJfdXNlcm5hbWUAeW91cl9wYXNzd29yZA==`
// data is `PLAIN AHlvdXJfdXNlcm5hbWUAeW91cl9wYXNzd29yZA==`, just without the command AUTH, which is stripped by the server, you have to handle the command in your controller
// Ok(Message) for successful authentication
// Err(Message) for failed authentication and the connection will be closed peacefully
pub async fn on_auth(conn: Arc<Mutex<SMTPConnection<ConnectionState>>>, data: String) -> Result<Message, Message> {
    let conn = conn.lock().await;
    let mut state = conn.state.lock().await;

    // Using our custom state
    state.authenticated = true;
    // We can also decide to not authenticate the user

    Ok(Message::builder()
        .status(neo_email::status_code::StatusCodes::AuthenticationSuccessful)
        .message("Authenticated".to_string())
        .build())
}

// This function is called when an email is received
// The mail is a struct that contains the email data, in this case the raw email data in a Vec<u8>
// Headers are parsed in a hashmap and the body is a Vec<u8>
pub async fn on_email(conn: Arc<Mutex<SMTPConnection<ConnectionState>>>, mail: Mail<Vec<u8>>) -> Message {
    let conn = conn.lock().await;
    let state = conn.state.lock().await;

    let headers = mail.headers.clone(); // get the hashmap
    let subject = headers.get(&EmailHeaders::Subject).unwrap(); // get the Option<Subject> header 
    println!("Subject: {:?}", subject);

    // Check if the user is authenticated from state set in on_auth
    if !state.authenticated {
        return Message::builder()
            .status(StatusCodes::AuthenticationCredetialsInvalid)
            .message("Authentication required".to_string())
            .build();
    }

    log::info!("Received email: {:?}", mail);
    Message::builder()
        .status(neo_email::status_code::StatusCodes::OK)
        .message("Email received".to_string())
        .build()
}

fn set_logger() -> Result<(), Box<dyn std::error::Error>> {
    Dispatch::new()
        // Perform allocation-free log formatting
        .format(|out, message, record| {
            let level_color = match record.level() {
                log::Level::Error => Color::Red,
                log::Level::Warn => Color::Yellow,
                log::Level::Info => Color::Green,
                log::Level::Debug => Color::Blue,
                log::Level::Trace => Color::Magenta,
            };
            
            // Format the log message
            let formatted_message = format!(
                "[{} {} {}] {}",
                humantime::format_rfc3339(std::time::SystemTime::now()),
                record.level(),
                record.target(),
                message
            );

            // Apply color to the message based on the log level
            out.finish(format_args!(
                "{}",
                formatted_message.color(level_color)
            ));
        })
        // Add blanket level filter -
        .level(log::LevelFilter::Trace)
        // - and per-module overrides
        .level_for("hyper", log::LevelFilter::Info)
        // Output to stdout
        .chain(std::io::stdout())
        // Apply globally
        .apply()?;
    
    // Example usage
    log::info!("This is an info message");
    log::error!("This is an error message");

    Ok(())
}
```