use std::error::Error; use colored::Colorize; use lettre::message::{header, SinglePart}; use lettre::transport::smtp::authentication::Credentials; use lettre::{Message, SmtpTransport, Transport}; use uuid::Uuid; use crate::config_spec::{CfgField, CfgGroup}; #[cfg(debug_assertions)] const TO_DEFAULT: &str = include_str!("debug_emails.txt"); // NB: make this file yourself; one address per line #[cfg(not(debug_assertions))] const TO_DEFAULT: &str = "CCCB Intern "; pub const CONFIG: CfgGroup<'static> = CfgGroup { name: "email", description: "Sending emails.", fields: &[ CfgField::Default { key: "server", default: "mail.berlin.ccc.de", description: "SMTP server used for sending emails.", }, CfgField::Default { key: "user", default: "plenum-bot@berlin.ccc.de", description: "User name used for authenticating with the mail server.", }, CfgField::Password { key: "password", description: "Password for authenticating with the mail server.", }, CfgField::Default { key: "from", default: "Plenumsbot ", description: "Email address to use for \"From:\".", }, CfgField::Default { key: "to", default: TO_DEFAULT, description: "Recipient of the emails sent.", }, CfgField::Optional { key: "last-message-id", description: "Message-Id of last initial announcement to send In-Reply-To (if applicable).", }, ], }; #[derive(Clone)] pub struct Email { server: String, credentials: Credentials, message_id_suffix: String, is_dry_run: bool, } pub struct SimpleEmail { base: Email, from: String, to: Vec, in_reply_to: Option, } impl SimpleEmail { pub fn new(base: Email, from: &str, to: &str, in_reply_to: Option) -> Self { let recipients = to.split('\n').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(); Self { base: base.clone(), from: from.to_string(), to: recipients, in_reply_to } } pub fn send_email(&self, subject: String, body: String) -> Result> { self.base.send_email( self.from.clone(), &self.to.clone(), subject, body, self.in_reply_to.clone(), ) } } impl Email { pub fn new(server: &str, user: &str, pass: &str, is_dry_run: bool) -> Self { let message_id_suffix = user.split('@').next_back().unwrap_or(&server).to_string(); let credentials = Credentials::new(user.to_string(), pass.to_string()); Self { server: server.to_string(), credentials, message_id_suffix, is_dry_run } } pub fn send_email( &self, from: String, to: &[String], subject: String, body: String, in_reply_to: Option, ) -> Result> { let message_id = Uuid::new_v4().to_string() + &self.message_id_suffix; let mut email = Message::builder() .from(from.parse().unwrap()); for recipient in to { email = email.to(recipient.parse().unwrap()); }; email = email.message_id(Some(message_id.clone())); let email = if in_reply_to.is_some() { email.in_reply_to(in_reply_to.unwrap()) } else { email } .subject(subject) .singlepart( SinglePart::builder().header(header::ContentType::TEXT_PLAIN).body(body), ) .unwrap(); if !self.is_dry_run { let mailer = SmtpTransport::starttls_relay(&self.server)? .credentials(self.credentials.clone()) .build(); mailer.send(&email)?; Ok(message_id) } else { println!( "{}\n(raw message:)\n{}", "[DRY RUN - NOT sending email]".yellow(), std::str::from_utf8(&email.formatted()).unwrap_or("((UTF-8 error))").blue() ); Ok("dummy-message-id@localhost".to_string()) } } }