//! mail functions use anyhow::Result; use bytes::Bytes; use bytes::BytesMut; use std::ffi::CString; use sequoia_openpgp::packet::UserID; use lettre::transport::smtp::{ client::SmtpConnection, commands::*, extension::ClientId, SMTP_PORT, }; use lettre::Address; use tokio::task::JoinSet; use std::time::Duration; use chrono::Utc; use crate::types::cheader::CHeader; use crate::types::defaults; use crate::types::errors::HuskError; use crate::types::recipient::Recipient; /// Parse the user id fields from a string. pub fn to_userid(rcpt: &CString) -> Result { let rcpt = rcpt.to_str()?; Ok(UserID::from(rcpt)) } /// Get the email from the string representation of a user id. pub fn to_email(rcpt: &CString) -> Result { let u = to_userid(rcpt)?; if let Some(e) = u.email()? { Ok(e.to_string()) } else { Err(HuskError::CannotConvertToEmail(u.to_string()).into()) } } /// Checks if the parameter has a valid email format. pub fn is_email(email: &str) -> bool { let uid = UserID::from(email); if let Ok(Some(e)) = uid.email() { e == email } else { false } } /// Checks if the parameter has a valid domain format. pub fn is_domain(domain: &str) -> bool { is_email(format!("test@{}", domain).as_str()) } pub async fn smtp_inject(from: String, to: &Vec>, header: CHeader, body: &Bytes) -> Result<()> { let mut joinset: JoinSet> = JoinSet::new(); log::debug!("smtp inject: from {}", from); // build message let mut header_size = 0; for (key, value) in header.iter_lines() { header_size += key.count_bytes(); header_size += value.count_bytes(); header_size += 4; // 4 is ': ' plus '\r\n' } // The size of the whole message the sum of header and body size + '\r\n' // as the dividing empty line. let mut message = BytesMut::with_capacity(header_size + body.len() + 2); for (key, value) in header.iter_lines() { message.extend_from_slice(key.as_bytes()); message.extend_from_slice(": ".as_bytes()); message.extend_from_slice(value.as_bytes()); message.extend_from_slice("\r\n".as_bytes()); } message.extend_from_slice(b"\r\n"); message.extend_from_slice(&body[..]); // convert addresses let from_email = CString::new(from.clone())?; let from_email = to_email(&from_email)?; let from_email: Address = from_email.try_into()?; let mut rcpts: Vec
= Vec::new(); for rcpt in to { let rcpt = rcpt.email.clone(); rcpts.push(rcpt.try_into()?); } // XXX: add secret token? let my_id = ClientId::Domain(defaults::HELO_REINJECT_ID.to_owned()); joinset.spawn(async move { // XXX // We connect to localhost - every other host would require implementing // TLS. The milter protocol is unencrypted anyways. match SmtpConnection::connect(("localhost", SMTP_PORT), Some(Duration::from_secs(5)), &my_id, None, None) { Ok(mut client) => { log::debug!("connected - start sending"); client.command(Mail::new(Some(from_email),vec![]))?; for rcpt in rcpts { client.command(Rcpt::new(rcpt, vec![]))?; } client.command(Data)?; client.message(&message)?; client.command(Quit)?; Ok(()) }, Err(e) => { log::error!("cannot connect to localhost:{} - {:?}", SMTP_PORT, e); Err(e.into()) } } }); match joinset.join_all().await.first() { Some(Ok(_)) => { log::debug!("mail injection done."); Ok(()) }, Some(Err(e)) => { log::error!("mail injection failed: {:?}", e); Err(HuskError::SmtpInjectionFailure(e.to_string()).into()) }, _ => { log::error!("mail injection failed without reason"); Err(HuskError::SmtpInjectionFailure("no reason".to_string()).into()) }, } } pub fn generate_footer(rcpts: &Vec) -> String { internal_generate_footer(Utc::now().to_rfc2822(), rcpts) } pub(crate) fn internal_generate_footer(now: String, other_rpcts: &Vec) -> String { let mut footer = defaults::FOOTER.replace("{date}", now.as_str()); if ! other_rpcts.is_empty() { let rcpts = other_rpcts.iter() .map(|r| r.email.clone()) .collect::>() .join(", "); footer.push_str( defaults::FOOTER_RECIPIENTS.replace( "{rcpts}", &rcpts).as_str() ); } footer } #[cfg(test)] pub mod tests { use crate::mail::internal_generate_footer; use crate::types::recipient::Recipient; #[test] fn test_footer() { let rcpts = vec![ Recipient { email: "alice@example.com".to_string(), certs: Vec::new() }, Recipient { email: "bob@example.com".to_string(), certs: Vec::new() } ]; let footer = internal_generate_footer( "1.1.1970".to_string(), &rcpts ); assert!(footer.contains("1.1.1970")); assert!(footer.contains("alice@example.com")); assert!(footer.contains("bob@example.com")); } }