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:
Jean Chevronnet 2026-07-12 15:57:34 +00:00
parent fbcf0eaac7
commit 06e9da4dc8
No known key found for this signature in database
10 changed files with 139 additions and 25 deletions

View file

@ -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();