// // Husk milter // // the actual daemon // use anyhow; use bytes::Bytes; use std::ffi::CString; use tokio::signal; use tokio::net::TcpListener; use indymilter::{ Actions, Callbacks, Context, EomContext, Macros, NegotiateContext, ProtoOpts, SocketInfo, Status, Config, ContextActions, }; use crate::types::husk_context::HuskContext; use crate::config::{HuskConfig, HuskConfigContainer}; use crate::mail; use crate::crypto; use crate::types::mail_context::ProtectionPossibility; pub struct Daemon { } impl Daemon { pub async fn run(config_container: HuskConfigContainer) -> anyhow::Result<()> { let config: HuskConfig = config_container.clone().into(); let listener = TcpListener::bind(&config.connection) .await .expect(" opening milter socket"); let callbacks = Callbacks::new() .on_negotiate(|cx, actions, opts| Box::pin(Self::handle_negotiate(cx, actions, opts))) .on_connect(|cx, hostname, socket_info| Box::pin(Self::handle_connect(cx, hostname, socket_info))) .on_helo(move |cx, hostname| Box::pin(Self::handle_helo(cx, config_container.clone(), hostname))) .on_mail(|cx, args| Box::pin(Self::handle_mail(cx, args))) .on_rcpt(|cx, args| Box::pin(Self::handle_rcpt(cx, args))) .on_data(|cx| Box::pin(Self::handle_data(cx))) .on_header(|cx, name, value| Box::pin(Self::handle_header(cx, name, value))) .on_eoh(|cx| Box::pin(Self::handle_eoh(cx))) .on_body(|cx, chunk| Box::pin(Self::handle_body(cx, chunk))) .on_eom(|cx| Box::pin(Self::handle_eom(cx))) .on_abort(|cx| Box::pin(Self::handle_abort(cx))) .on_close(|cx| Box::pin(Self::handle_close(cx))) .on_unknown(|cx, arg| Box::pin(Self::handle_unknown(cx, arg))); let config = Default::default(); indymilter::run(listener, callbacks, config, signal::ctrl_c()) .await .expect("milter execution failed"); Ok(()) } async fn handle_negotiate( _cx: &mut NegotiateContext>, actions: Actions, opts: ProtoOpts, ) -> Status { log::debug!("NEGOTIATE: actions: {actions:?} - opts: {opts:?}"); Status::AllOpts } async fn handle_connect( _cx: &mut Context>, hostname: CString, socket_info: SocketInfo, ) -> Status { log::debug!("CONNECT: hostname: {hostname:?} - socket_info: {socket_info:?}"); //Self::print_macros(&cx.macros); Status::Continue } async fn handle_helo(cx: &mut Context>, config_container: HuskConfigContainer, hostname: CString) -> Status { log::debug!("HELO: hostname: {hostname:?}"); let config: HuskConfig = config_container.into(); // setup Context match HuskContext::new(&config) { Ok(context) => { log::debug!("found introducers: {:?}", context.introducers); cx.data = Some(context).take(); Status::Continue }, Err(e) => { log::error!("{}", e); Status::Tempfail } } } async fn handle_mail(cx: &mut Context>, args: Vec) -> Status { log::debug!("MAIL: {args:?}"); let mut sender: Option = None; for arg in args { if let Some(s) = mail::to_email(&arg) { sender = Some(s); break; } } if sender.is_none() { return Status::Tempfail; } if let Some(ref mut context) = cx.data { context.reset_mail(); context.set_sender(sender.unwrap()); Status::Continue } else { Status::Tempfail } } async fn handle_rcpt(cx: &mut Context>, args: Vec) -> Status { log::debug!("RCPT: {args:?}"); if let Some(ref mut context) = cx.data { let mut rcpt: Option = None; for arg in args { if let Some(s) = mail::to_email(&arg) { rcpt = Some(s); break; } } if rcpt.is_none() { Status::Tempfail } else { context.add_recipient(rcpt.unwrap()).await; Status::Continue } } else { Status::Tempfail } } async fn handle_data(cx: &mut Context>) -> Status { log::debug!("DATA"); if let Some(ref mut _context) = cx.data { // XXX: check for early accepting // if there are no recipients with authenticated certificates // tell the MTA that this milter is done and the mail can be further // processed. Status::Continue } else { Status::Tempfail } } async fn handle_header(_cx: &mut Context>, name: CString, value: CString) -> Status { log::debug!("HEADER: {name:?} = {value:?}"); Status::Continue } async fn handle_eoh(_cx: &mut Context>) -> Status { log::debug!("EOH"); Status::Continue } async fn handle_body(cx: &mut Context>, chunk: Bytes) -> Status { log::debug!("BODY: chunk with {:?} bytes received", &chunk.len()); if let Some(ref mut context) = cx.data { context.mail.add_chunk(chunk); Status::Continue } else { Status::Tempfail } } async fn handle_eom(cx: &mut EomContext>) -> Status { log::debug!("EOM"); if let Some(ref mut context) = cx.data { log::debug!("Mail from {:?} complete", context.mail.sender); log::debug!("recipients: {:?}", context.mail.recipients); log::debug!("body size: {}", context.mail.body_size); let body = context.mail.collect_body(); // check if mail is already encrypted. If it is, keep it as such. if crypto::is_encrypted(&body) { log::info!("encrypted mail detected"); return Status::Accept; } // possible protection // none -> accept // partial -> accept (later: split) // full -> encrypt let protection = ProtectionPossibility::from(&context.mail); if protection == ProtectionPossibility::Full { // if an error occures the body is not exchanged match crypto::encrypt(&context, &body, &context.mail.recipients) { Ok(encrypted) => { if cx.actions.replace_body(&encrypted).await.is_err() { log::error!("Cannot exchange body"); } }, Err(e) => { log::error!("{}", e); } } } } Status::Continue } async fn handle_abort(_cx: &mut Context>) -> Status { log::debug!("ABORT"); Status::Continue } async fn handle_close(_cx: &mut Context>) -> Status { log::debug!("CLOSE"); Status::Continue } async fn handle_unknown(_cx: &mut Context>, arg: CString) -> Status { log::debug!("UNKNOWN: {arg:?}"); Status::Continue } fn print_macros(macros: &Macros) { println!(" macros: {:?}", macros.to_hash_map()); } }