| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
use crate::config::Email; |
| 6 |
use crate::Shared; |
| 7 |
|
| 8 |
pub const SETTING_SMTP: &str = "mail.smtp"; |
| 9 |
pub const SETTING_FROM: &str = "mail.from"; |
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
pub async fn load(db: &crate::db::Pool, from_config: &Email) -> Email { |
| 14 |
let mut mail = from_config.clone(); |
| 15 |
if let Ok(Some(s)) = crate::db::accounts::setting(db, SETTING_SMTP).await { |
| 16 |
mail.smtp = s; |
| 17 |
mail.from = crate::db::accounts::setting(db, SETTING_FROM).await.ok().flatten().unwrap_or_default(); |
| 18 |
} |
| 19 |
mail |
| 20 |
} |
| 21 |
|
| 22 |
pub fn settings(app: &Shared) -> Email { |
| 23 |
app.mail.read().unwrap_or_else(|p| p.into_inner()).clone() |
| 24 |
} |
| 25 |
|
| 26 |
|
| 27 |
|
| 28 |
pub async fn save(app: &Shared, smtp: &str, from: &str) -> sqlx::Result<()> { |
| 29 |
crate::db::accounts::set_setting(&app.db, SETTING_SMTP, smtp).await?; |
| 30 |
crate::db::accounts::set_setting(&app.db, SETTING_FROM, from).await?; |
| 31 |
*app.mail.write().unwrap_or_else(|p| p.into_inner()) = Email { smtp: smtp.to_string(), from: from.to_string() }; |
| 32 |
Ok(()) |
| 33 |
} |
| 34 |
|
| 35 |
|
| 36 |
pub fn masked(smtp: &str) -> String { |
| 37 |
let Some((scheme, rest)) = smtp.split_once("://") else { return smtp.to_string() }; |
| 38 |
let Some((userinfo, host)) = rest.rsplit_once('@') else { return smtp.to_string() }; |
| 39 |
match userinfo.split_once(':') { |
| 40 |
Some((user, _)) => format!("{scheme}://{user}:***@{host}"), |
| 41 |
None => smtp.to_string(), |
| 42 |
} |
| 43 |
} |
| 44 |
|
| 45 |
|
| 46 |
pub fn enabled(app: &Shared) -> bool { |
| 47 |
let cfg = settings(app); |
| 48 |
!cfg.smtp.is_empty() && !cfg.from.is_empty() |
| 49 |
} |
| 50 |
|
| 51 |
|
| 52 |
pub async fn send(app: &Shared, to_name: &str, to_addr: &str, subject: &str, body: &str) -> Result<(), String> { |
| 53 |
if !enabled(app) { |
| 54 |
return Err("this hub does not send mail".into()); |
| 55 |
} |
| 56 |
let sent = deliver(app, to_name, to_addr, subject, body, action, unsubscribe).await; |
| 57 |
let counter = if sent.is_ok() { &crate::metrics::MAIL_SENT } else { &crate::metrics::MAIL_FAILED }; |
| 58 |
counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed); |
| 59 |
sent |
| 60 |
} |
| 61 |
|
| 62 |
async fn deliver( |
| 63 |
app: &Shared, |
| 64 |
to_name: &str, |
| 65 |
to_addr: &str, |
| 66 |
subject: &str, |
| 67 |
body: &str, |
| 68 |
action: Option<Action<'_>>, |
| 69 |
unsubscribe: Option<&str>, |
| 70 |
) -> Result<(), String> { |
| 71 |
let cfg = settings(app); |
| 72 |
let subject = format!("[{}] {subject}", app.config.site.name); |
| 73 |
if let Some(dir) = cfg.smtp.strip_prefix("file://") { |
| 74 |
let name = format!("{}-{}.eml", crate::db::now_ms(), crate::auth::random_hex(4)); |
| 75 |
let text = format!("From: {}\nTo: {to_name} <{to_addr}>\nSubject: {subject}\n\n{body}", cfg.from); |
| 76 |
return tokio::fs::write(std::path::Path::new(dir).join(name), text).await.map_err(|e| format!("write mail to {dir}: {e}")); |
| 77 |
} |
| 78 |
use lettre::message::header::ContentType; |
| 79 |
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor}; |
| 80 |
let from = cfg.from.parse().map_err(|e| format!("[email] from: {e}"))?; |
| 81 |
let to = format!("{to_name} <{to_addr}>").parse().or_else(|_| to_addr.parse()).map_err(|e| format!("address {to_addr}: {e}"))?; |
| 82 |
let message = Message::builder() |
| 83 |
.from(from) |
| 84 |
.to(to) |
| 85 |
.subject(subject) |
| 86 |
.header(ContentType::TEXT_PLAIN) |
| 87 |
.body(body.to_string()) |
| 88 |
.map_err(|e| e.to_string())?; |
| 89 |
let transport = AsyncSmtpTransport::<Tokio1Executor>::from_url(&cfg.smtp).map_err(|e| format!("[email] smtp: {e}"))?.build(); |
| 90 |
transport.send(message).await.map(|_| ()).map_err(|e| format!("sending to {to_addr}: {e}")) |
| 91 |
} |