From a8e943c9a7abbde95c900d89fc5f2ed825e9afa3 Mon Sep 17 00:00:00 2001 From: Joris Date: Sat, 23 Jan 2021 08:58:42 +0100 Subject: Fix mail sending The error was due to the utilization of the special character “—” in the subject, with postfix’ sendmail. Also directly use process commands to get async support. --- src/jobs/weekly_report.rs | 11 +++- src/mail.rs | 126 ++++++++++++++++++++++++++++++---------------- src/model/config.rs | 2 +- 3 files changed, 94 insertions(+), 45 deletions(-) (limited to 'src') diff --git a/src/jobs/weekly_report.rs b/src/jobs/weekly_report.rs index 819d30b..b482f0d 100644 --- a/src/jobs/weekly_report.rs +++ b/src/jobs/weekly_report.rs @@ -17,10 +17,17 @@ pub async fn send( let users = db::users::list(pool).await; mail::send( config, - users.into_iter().map(|u| (u.email, u.name)).collect(), - "Budget — rapport hebdomadaire".to_string(), + users + .into_iter() + .map(|u| mail::Recipient { + name: u.name, + address: u.email, + }) + .collect(), + "Rapport hebdomadaire".to_string(), report, ) + .await } Err(err) => { error!("Error preparing weekly report from template: {:?}", err); diff --git a/src/mail.rs b/src/mail.rs index 52b5789..149a5ef 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -1,59 +1,101 @@ -use lettre::sendmail::SendmailTransport; -use lettre::{SendableEmail, Transport}; -use lettre_email::Email; +use chrono::Utc; +use std::io::{Error, ErrorKind}; +use std::process::{Output, Stdio}; +use tokio::io::AsyncWriteExt; +use tokio::process::Command; use crate::model::config::Config; -static FROM: &str = "contact@guyonvarch.me"; +static FROM_NAME: &str = "Budget"; +static FROM_ADDRESS: &str = "budget@guyonvarch.me"; -pub fn send( +#[derive(Clone)] +pub struct Recipient { + pub name: String, + pub address: String, +} + +pub async fn send( config: &Config, - to: Vec<(String, String)>, + recipients: Vec, subject: String, message: String, ) -> bool { - match prepare_email(to.clone(), subject.clone(), message.clone()) { - Ok(email) => { - if let Some(sendmail_path) = &config.sendmail_path { - let mut sender = - SendmailTransport::new_with_command(sendmail_path); - match sender.send(email) { - Ok(_) => true, - Err(err) => { - error!("Error sending email: {:?}", err); - false - } + let headers = format_headers(recipients.clone(), subject); + + info!( + "Sending mail{}\n{}", + if config.mock_mails { " (MOCK)" } else { "" }, + headers.clone() + ); + + if config.mock_mails { + true + } else { + let recipient_addresses = recipients + .clone() + .into_iter() + .map(|r| r.address) + .collect::>(); + + let mut command = Command::new("sendmail"); + command.kill_on_drop(true); + command.arg("-f").arg(FROM_ADDRESS); + command.arg("--").args(recipient_addresses); + command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let message = format!("{}\n\n{}", headers, message); + match spawn(command, &message.into_bytes()).await { + Ok(output) => { + if output.status.success() { + info!("Mail sent"); + true + } else { + match String::from_utf8(output.stderr) { + Ok(error) => error!("Error sending email: {}", error), + _ => error!("Error sending email"), + }; + false } - } else { - let formatted_to = to - .into_iter() - .map(|t| t.0) - .collect::>() - .join(", "); - info!( - "MOCK MAIL\nto: {}\nsubject: {}\n\n{}", - formatted_to, subject, message - ); - true } - } - Err(err) => { - error!("Error preparing email: {:?}", err); - false + Err(err) => { + error!("Error spawning command: {:?}", err); + false + } } } } -fn prepare_email( - to: Vec<(String, String)>, - subject: String, - message: String, -) -> Result { - let mut email = Email::builder().from(FROM).subject(subject).text(message); +fn format_headers(recipients: Vec, subject: String) -> String { + let recipients = recipients + .into_iter() + .map(|r| format_address(r.name, r.address)) + .collect::>() + .join(", "); - for (address, name) in to.iter() { - email = email.to((address, name)); - } + format!( + "Date: {}\nFrom: {}\nTo: {}\nSubject: {}", + Utc::now().to_rfc2822(), + format_address(FROM_NAME.to_string(), FROM_ADDRESS.to_string()), + recipients, + subject, + ) +} + +fn format_address(name: String, address: String) -> String { + format!("{} <{}>", name, address) +} - email.build().map(|e| e.into()) +async fn spawn(mut command: Command, stdin: &Vec) -> Result { + let mut process = command.spawn()?; + process + .stdin + .as_mut() + .ok_or(Error::new(ErrorKind::Other, "Getting mutable stdin"))? + .write_all(stdin) + .await?; + process.wait_with_output().await } diff --git a/src/model/config.rs b/src/model/config.rs index 2c5455e..cebe1a6 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -3,5 +3,5 @@ use serde::Deserialize; #[derive(Clone, Deserialize)] pub struct Config { pub secure_cookies: bool, - pub sendmail_path: Option, + pub mock_mails: bool, } -- cgit v1.2.3