diff --git a/modules/nickserv/resetpass.rs b/modules/nickserv/resetpass.rs index 67fd242..215d872 100644 --- a/modules/nickserv/resetpass.rs +++ b/modules/nickserv/resetpass.rs @@ -19,7 +19,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: return; }; let code = db.issue_code(&canonical, CodeKind::Reset); - let mail = crate::email::reset(db.email_brand(), db.email_accent(), &canonical, &code); + let mail = crate::email::reset(db.email_brand(), db.email_accent(), db.email_logo(), &canonical, &code); ctx.send_email(email, mail.subject, mail.text, Some(mail.html)); ctx.notice(me, from.uid, format!("A reset code has been emailed to the address on file for \x02{canonical}\x02.")); } diff --git a/src/config.rs b/src/config.rs index b17e376..9a28e5b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -27,6 +27,10 @@ pub struct Email { // Accent colour (any CSS colour) for the email template. #[serde(default = "default_accent")] pub accent: String, + // Optional logo image URL shown in the email header (must be a hosted image; + // email clients don't render inline SVG or data URIs). + #[serde(default)] + pub logo: String, } fn default_brand() -> String { diff --git a/src/email.rs b/src/email.rs index 1bd5514..216a306 100644 --- a/src/email.rs +++ b/src/email.rs @@ -9,8 +9,23 @@ pub struct Mail { const BASE: &str = include_str!("../templates/email/base.html"); -fn render(brand: &str, accent: &str, title: &str, message: &str, code: &str, note: &str) -> String { - BASE.replace("{{brand}}", &escape(brand)) +// The masthead: a logo image beside the brand name when a logo URL is set, +// otherwise the brand name as an accent eyebrow. +fn brand_mark(brand: &str, accent: &str, logo: &str) -> String { + let (brand, accent) = (escape(brand), escape(accent)); + if logo.is_empty() { + format!("{brand}") + } else { + format!( + "\"\"{brand}", + escape(logo) + ) + } +} + +fn render(brand: &str, accent: &str, logo: &str, title: &str, message: &str, code: &str, note: &str) -> String { + BASE.replace("{{brand_mark}}", &brand_mark(brand, accent, logo)) + .replace("{{brand}}", &escape(brand)) .replace("{{accent}}", &escape(accent)) .replace("{{title}}", &escape(title)) .replace("{{message}}", &escape(message)) @@ -18,7 +33,7 @@ fn render(brand: &str, accent: &str, title: &str, message: &str, code: &str, not .replace("{{note}}", &escape(note)) } -pub fn reset(brand: &str, accent: &str, account: &str, code: &str) -> Mail { +pub fn reset(brand: &str, accent: &str, logo: &str, account: &str, code: &str) -> Mail { Mail { subject: format!("Password reset for {account}"), text: format!( @@ -27,6 +42,7 @@ pub fn reset(brand: &str, accent: &str, account: &str, code: &str) -> Mail { html: render( brand, accent, + logo, "Password reset", &format!("Use this code to reset the password for your account {account}."), code, @@ -35,7 +51,7 @@ pub fn reset(brand: &str, accent: &str, account: &str, code: &str) -> Mail { } } -pub fn confirm(brand: &str, accent: &str, account: &str, code: &str) -> Mail { +pub fn confirm(brand: &str, accent: &str, logo: &str, account: &str, code: &str) -> Mail { Mail { subject: format!("Confirm your {account} registration"), text: format!( @@ -44,6 +60,7 @@ pub fn confirm(brand: &str, accent: &str, account: &str, code: &str) -> Mail { html: render( brand, accent, + logo, "Confirm your account", &format!("Welcome! Confirm the email for your account {account} with the code below."), code, diff --git a/src/engine/db.rs b/src/engine/db.rs index 20e441b..1e43f7f 100644 --- a/src/engine/db.rs +++ b/src/engine/db.rs @@ -456,9 +456,10 @@ pub struct Db { pub(crate) scram_iterations: u32, // Whether outbound email is configured, so email features can gate themselves. email_enabled: bool, - // Display name and accent colour for email templates. + // Display name, accent colour, and optional logo URL for email templates. email_brand: String, email_accent: String, + email_logo: String, // Node-local, non-persisted email codes: account -> (purpose, code, expiry). codes: HashMap, } @@ -507,7 +508,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, email_brand: "Network Services".to_string(), email_accent: "#4f46e5".to_string(), codes: HashMap::new() } + Self { accounts, channels, grouped, log, scram_iterations: scram::DEFAULT_ITERATIONS, email_enabled: false, email_brand: "Network Services".to_string(), email_accent: "#4f46e5".to_string(), email_logo: String::new(), codes: HashMap::new() } } /// Fold an entry authored by another node into the store — the services-side @@ -744,6 +745,11 @@ impl Db { &self.email_accent } + /// Logo image URL for email templates (empty = show the brand name as text). + pub fn email_logo(&self) -> &str { + &self.email_logo + } + pub fn set_email_brand(&mut self, brand: &str) { self.email_brand = brand.to_string(); } @@ -752,6 +758,10 @@ impl Db { self.email_accent = accent.to_string(); } + pub fn set_email_logo(&mut self, logo: &str) { + self.email_logo = logo.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(); diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 361d9eb..a3a7a4a 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -548,7 +548,7 @@ 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); - let mail = crate::email::confirm(self.db.email_brand(), self.db.email_accent(), account, &code); + let mail = crate::email::confirm(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), 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 \x02.".to_string() }); } diff --git a/src/main.rs b/src/main.rs index e41ca2a..f4dbce4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,6 +60,7 @@ async fn main() -> Result<()> { if let Some(email) = &cfg.email { db.set_email_brand(&email.brand); db.set_email_accent(&email.accent); + db.set_email_logo(&email.logo); } let engine = Arc::new(Mutex::new(Engine::new(services, db))); diff --git a/templates/email/base.html b/templates/email/base.html index 1a4d150..c812797 100644 --- a/templates/email/base.html +++ b/templates/email/base.html @@ -14,7 +14,7 @@   - {{brand}} +
{{brand_mark}}

{{title}}

{{message}}