email: HTML templates and multipart messages
Emails now render an HTML template from templates/email with a plaintext fallback, sent as multipart/alternative. A configurable brand name shows in the template. Reset and confirmation mails share one styled base.
This commit is contained in:
parent
fbcf0eaac7
commit
06e9da4dc8
10 changed files with 139 additions and 25 deletions
|
|
@ -456,6 +456,8 @@ pub struct Db {
|
|||
pub(crate) scram_iterations: u32,
|
||||
// Whether outbound email is configured, so email features can gate themselves.
|
||||
email_enabled: bool,
|
||||
// Display name for email templates.
|
||||
email_brand: String,
|
||||
// Node-local, non-persisted email codes: account -> (purpose, code, expiry).
|
||||
codes: HashMap<String, (CodeKind, String, Instant)>,
|
||||
}
|
||||
|
|
@ -504,7 +506,7 @@ impl Db {
|
|||
apply(&mut accounts, &mut channels, &mut grouped, event);
|
||||
}
|
||||
tracing::info!(accounts = accounts.len(), channels = channels.len(), "account store loaded");
|
||||
Self { accounts, channels, grouped, log, scram_iterations: scram::DEFAULT_ITERATIONS, email_enabled: false, codes: HashMap::new() }
|
||||
Self { accounts, channels, grouped, log, scram_iterations: scram::DEFAULT_ITERATIONS, email_enabled: false, email_brand: "Network Services".to_string(), codes: HashMap::new() }
|
||||
}
|
||||
|
||||
/// Fold an entry authored by another node into the store — the services-side
|
||||
|
|
@ -731,6 +733,15 @@ impl Db {
|
|||
self.email_enabled = on;
|
||||
}
|
||||
|
||||
/// Display name used in email templates.
|
||||
pub fn email_brand(&self) -> &str {
|
||||
&self.email_brand
|
||||
}
|
||||
|
||||
pub fn set_email_brand(&mut self, brand: &str) {
|
||||
self.email_brand = brand.to_string();
|
||||
}
|
||||
|
||||
/// Issue a fresh emailed code for `account` and purpose, valid for 15 minutes.
|
||||
pub fn issue_code(&mut self, account: &str, kind: CodeKind) -> String {
|
||||
let code = gen_code();
|
||||
|
|
|
|||
|
|
@ -548,11 +548,8 @@ impl Engine {
|
|||
if ok && !self.db.is_verified(account) {
|
||||
if let (Some(addr), RegReply::NickServ { agent, uid, .. }) = (addr, &reply) {
|
||||
let code = self.db.issue_code(account, db::CodeKind::Confirm);
|
||||
out.push(NetAction::SendEmail {
|
||||
to: addr,
|
||||
subject: format!("Confirm your {account} registration"),
|
||||
body: format!("Welcome! Confirm your account {account} with:\n /msg NickServ CONFIRM {code}\nThe code expires in 15 minutes."),
|
||||
});
|
||||
let mail = crate::email::confirm(self.db.email_brand(), account, &code);
|
||||
out.push(NetAction::SendEmail { to: addr, subject: mail.subject, text: mail.text, html: Some(mail.html) });
|
||||
out.push(NetAction::Notice { from: agent.clone(), to: uid.clone(), text: "A confirmation code has been emailed to you. Confirm with \x02CONFIRM <code>\x02.".to_string() });
|
||||
}
|
||||
}
|
||||
|
|
@ -1237,7 +1234,7 @@ mod tests {
|
|||
// Request: a code is emailed to the address on file.
|
||||
let out = to_ns(&mut e, "RESETPASS alice");
|
||||
let (to, body) = out.iter().find_map(|a| match a {
|
||||
NetAction::SendEmail { to, body, .. } => Some((to.clone(), body.clone())),
|
||||
NetAction::SendEmail { to, text, .. } => Some((to.clone(), text.clone())),
|
||||
_ => None,
|
||||
}).expect("a reset code is emailed");
|
||||
assert_eq!(to, "alice@example.org");
|
||||
|
|
@ -1279,7 +1276,7 @@ mod tests {
|
|||
let out = e.complete_register(&account, Db::derive_credentials(&password, 4096), email, reply);
|
||||
assert!(!e.db.is_verified("newbie"), "registering with email starts unverified");
|
||||
let body = out.iter().find_map(|a| match a {
|
||||
NetAction::SendEmail { body, .. } => Some(body.clone()),
|
||||
NetAction::SendEmail { text, .. } => Some(text.clone()),
|
||||
_ => None,
|
||||
}).expect("a confirm code is emailed");
|
||||
let code = body.split("CONFIRM ").nth(1).unwrap().split_whitespace().next().unwrap().to_string();
|
||||
|
|
|
|||
|
|
@ -58,8 +58,8 @@ impl ServiceCtx {
|
|||
}
|
||||
|
||||
// Send an email (the link layer pipes it to the configured mail command).
|
||||
pub fn send_email(&mut self, to: impl Into<String>, subject: impl Into<String>, body: impl Into<String>) {
|
||||
self.actions.push(NetAction::SendEmail { to: to.into(), subject: subject.into(), body: body.into() });
|
||||
pub fn send_email(&mut self, to: impl Into<String>, subject: impl Into<String>, text: impl Into<String>, html: Option<String>) {
|
||||
self.actions.push(NetAction::SendEmail { to: to.into(), subject: subject.into(), text: text.into(), html });
|
||||
}
|
||||
|
||||
// Hand a password change to the engine to finish: its derivation runs off the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue