plenum-bot/src/email.rs

125 lines
4.2 KiB
Rust
Raw Normal View History

2024-08-05 19:29:02 +02:00
use std::error::Error;
use colored::Colorize;
use lettre::message::{header, SinglePart};
use lettre::transport::smtp::authentication::Credentials;
2024-08-06 18:05:26 +02:00
use lettre::{Message, SmtpTransport, Transport};
use uuid::Uuid;
2024-08-05 19:29:02 +02:00
use crate::config_spec::{CfgField, CfgGroup};
2024-08-06 21:51:53 +02:00
#[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 <intern@berlin.ccc.de>";
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 {
2024-08-05 19:29:02 +02:00
key: "from",
default: "Plenumsbot <plenum-bot@berlin.ccc.de>",
description: "Email address to use for \"From:\".",
},
CfgField::Default {
key: "to",
2024-08-06 21:51:53 +02:00
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,
2024-08-06 21:51:53 +02:00
to: Vec<String>,
in_reply_to: Option<String>,
}
impl SimpleEmail {
pub fn new(base: Email, from: &str, to: &str, in_reply_to: Option<String>) -> Self {
2024-08-07 03:59:45 +02:00
let recipients =
to.split('\n').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
2024-08-06 21:51:53 +02:00
Self { base: base.clone(), from: from.to_string(), to: recipients, in_reply_to }
}
pub fn send_email(&self, subject: String, body: String) -> Result<String, Box<dyn Error>> {
self.base.send_email(
self.from.clone(),
2024-08-06 21:51:53 +02:00
&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(
2024-08-07 03:59:45 +02:00
&self, from: String, to: &[String], subject: String, body: String,
in_reply_to: Option<String>,
) -> Result<String, Box<dyn Error>> {
let message_id = Uuid::new_v4().to_string() + &self.message_id_suffix;
2024-08-07 03:59:45 +02:00
let mut email = Message::builder().from(from.parse().unwrap());
2024-08-06 21:51:53 +02:00
for recipient in to {
email = email.to(recipient.parse().unwrap());
2024-08-07 03:59:45 +02:00
}
2024-08-06 21:51:53 +02:00
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())
}
}
}