106 lines
3 KiB
Rust
106 lines
3 KiB
Rust
use lettre::message::Mailbox;
|
|
use lettre::transport::smtp::client::Tls;
|
|
use lettre::{Message, SmtpTransport, Transport};
|
|
|
|
pub fn send_notification(
|
|
id: i64,
|
|
question: &str,
|
|
notify_email: &str,
|
|
mail_domain: &str,
|
|
reply_domain: &str,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let truncated = if question.len() > 50 {
|
|
format!("{}...", &question[..50])
|
|
} else {
|
|
question.to_string()
|
|
};
|
|
|
|
let from: Mailbox = format!("Q&A <qa@{mail_domain}>").parse()?;
|
|
let reply_to: Mailbox = format!("qa@{reply_domain}").parse()?;
|
|
let to: Mailbox = notify_email.parse()?;
|
|
|
|
let email = Message::builder()
|
|
.from(from)
|
|
.reply_to(reply_to)
|
|
.to(to)
|
|
.subject(format!("{id} - {truncated}"))
|
|
.body(question.to_string())?;
|
|
|
|
let mailer = SmtpTransport::builder_dangerous("localhost")
|
|
.tls(Tls::None)
|
|
.hello_name(lettre::transport::smtp::extension::ClientId::Domain(
|
|
mail_domain.to_string(),
|
|
))
|
|
.build();
|
|
mailer.send(&email)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn extract_id_from_subject(subject: &str) -> Result<i64, Box<dyn std::error::Error>> {
|
|
let subject = subject.trim();
|
|
let start = subject
|
|
.char_indices()
|
|
.find_map(|(idx, ch)| ch.is_ascii_digit().then_some(idx))
|
|
.ok_or("Subject missing question id")?;
|
|
let digits: String = subject[start..]
|
|
.chars()
|
|
.take_while(|c| c.is_ascii_digit())
|
|
.collect();
|
|
|
|
if digits.is_empty() {
|
|
return Err("Subject missing numeric question id".into());
|
|
}
|
|
|
|
let remainder = subject[start + digits.len()..].trim_start();
|
|
if !(remainder.starts_with('-') || remainder.starts_with(':')) {
|
|
return Err("Subject missing separator after question id".into());
|
|
}
|
|
|
|
Ok(digits.parse()?)
|
|
}
|
|
|
|
pub fn extract_plain_text_body(contents: &str) -> String {
|
|
let normalized = contents.replace("\r\n", "\n");
|
|
let body = if let Some((headers, body)) = normalized.split_once("\n\n") {
|
|
if headers.lines().any(|line| line.contains(':')) {
|
|
body
|
|
} else {
|
|
normalized.as_str()
|
|
}
|
|
} else {
|
|
normalized.as_str()
|
|
};
|
|
|
|
strip_quoted_text(body)
|
|
}
|
|
|
|
pub fn strip_quoted_text(body: &str) -> String {
|
|
let mut result = Vec::new();
|
|
for line in body.lines() {
|
|
if line.starts_with('>') {
|
|
continue;
|
|
}
|
|
if line.starts_with("On ") && line.ends_with("wrote:") {
|
|
break;
|
|
}
|
|
result.push(line);
|
|
}
|
|
result.join("\n").trim().to_string()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{extract_id_from_subject, extract_plain_text_body};
|
|
|
|
#[test]
|
|
fn extracts_id_from_subject() {
|
|
assert_eq!(extract_id_from_subject("Re: 42 - Hello").unwrap(), 42);
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_plain_text_from_raw_email() {
|
|
let raw = "Subject: Q&A #42: Hello\r\nFrom: Jet <jet@example.com>\r\n\r\nThis is the answer.\r\n\r\nOn earlier mail wrote:\r\n> quoted";
|
|
assert_eq!(extract_plain_text_body(raw), "This is the answer.");
|
|
}
|
|
}
|