From 98630c7d36b84aa9c4d187f17dc117b7acc2fcac Mon Sep 17 00:00:00 2001 From: Jean Date: Sun, 12 Jul 2026 16:07:36 +0000 Subject: [PATCH] email: premium HTML template with accent colour and copy affordance Redesigns the mail template (branded header bar, hero code block, copy button, muted footer) and adds a configurable accent colour alongside the brand name. --- modules/nickserv/resetpass.rs | 2 +- src/config.rs | 7 +++++ src/email.rs | 11 +++++--- src/engine/db.rs | 14 ++++++++-- src/engine/mod.rs | 2 +- src/main.rs | 1 + templates/email/base.html | 48 +++++++++++++++++++++++++---------- 7 files changed, 63 insertions(+), 22 deletions(-) diff --git a/modules/nickserv/resetpass.rs b/modules/nickserv/resetpass.rs index 109faa7..67fd242 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(), &canonical, &code); + let mail = crate::email::reset(db.email_brand(), db.email_accent(), &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 a7d00ae..b17e376 100644 --- a/src/config.rs +++ b/src/config.rs @@ -24,12 +24,19 @@ pub struct Email { // Display name shown in email templates (header/footer). #[serde(default = "default_brand")] pub brand: String, + // Accent colour (any CSS colour) for the email template. + #[serde(default = "default_accent")] + pub accent: String, } fn default_brand() -> String { "Network Services".to_string() } +fn default_accent() -> String { + "#4f46e5".to_string() +} + #[derive(Debug, Deserialize, Clone)] pub struct Gossip { // Address to accept peer connections on. Absent = dial-only node. diff --git a/src/email.rs b/src/email.rs index ac3218c..1bd5514 100644 --- a/src/email.rs +++ b/src/email.rs @@ -9,15 +9,16 @@ pub struct Mail { const BASE: &str = include_str!("../templates/email/base.html"); -fn render(brand: &str, title: &str, message: &str, code: &str, note: &str) -> String { +fn render(brand: &str, accent: &str, title: &str, message: &str, code: &str, note: &str) -> String { BASE.replace("{{brand}}", &escape(brand)) + .replace("{{accent}}", &escape(accent)) .replace("{{title}}", &escape(title)) .replace("{{message}}", &escape(message)) .replace("{{code}}", &escape(code)) .replace("{{note}}", &escape(note)) } -pub fn reset(brand: &str, account: &str, code: &str) -> Mail { +pub fn reset(brand: &str, accent: &str, account: &str, code: &str) -> Mail { Mail { subject: format!("Password reset for {account}"), text: format!( @@ -25,15 +26,16 @@ pub fn reset(brand: &str, account: &str, code: &str) -> Mail { ), html: render( brand, + accent, "Password reset", &format!("Use this code to reset the password for your account {account}."), code, - "This code expires in 15 minutes.", + "This code expires in 15 minutes. If you didn't ask to reset it, ignore this email.", ), } } -pub fn confirm(brand: &str, account: &str, code: &str) -> Mail { +pub fn confirm(brand: &str, accent: &str, account: &str, code: &str) -> Mail { Mail { subject: format!("Confirm your {account} registration"), text: format!( @@ -41,6 +43,7 @@ pub fn confirm(brand: &str, account: &str, code: &str) -> Mail { ), html: render( brand, + accent, "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 945dcfb..20e441b 100644 --- a/src/engine/db.rs +++ b/src/engine/db.rs @@ -456,8 +456,9 @@ 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. + // Display name and accent colour for email templates. email_brand: String, + email_accent: String, // Node-local, non-persisted email codes: account -> (purpose, code, expiry). codes: HashMap, } @@ -506,7 +507,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(), 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(), codes: HashMap::new() } } /// Fold an entry authored by another node into the store — the services-side @@ -738,10 +739,19 @@ impl Db { &self.email_brand } + /// Accent colour used in email templates. + pub fn email_accent(&self) -> &str { + &self.email_accent + } + pub fn set_email_brand(&mut self, brand: &str) { self.email_brand = brand.to_string(); } + pub fn set_email_accent(&mut self, accent: &str) { + self.email_accent = accent.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 b91ebbb..361d9eb 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(), account, &code); + let mail = crate::email::confirm(self.db.email_brand(), self.db.email_accent(), 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 aac71ea..e41ca2a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,6 +59,7 @@ async fn main() -> Result<()> { db.set_email_enabled(cfg.email.is_some()); if let Some(email) = &cfg.email { db.set_email_brand(&email.brand); + db.set_email_accent(&email.accent); } let engine = Arc::new(Mutex::new(Engine::new(services, db))); diff --git a/templates/email/base.html b/templates/email/base.html index e8609da..1a4d150 100644 --- a/templates/email/base.html +++ b/templates/email/base.html @@ -3,26 +3,46 @@ + {{title}} - - + +
- -
- {{brand}} + + + + + - + + + + + + - +
 
+ {{brand}} +

{{title}}

+

{{message}}

-

{{title}}

-

{{message}}

-
- {{code}} + +
+ + +
+
Your code
+
{{code}}
+
+
+
📋  Copy code
+
Tap and hold the code to copy it.
+
+

{{note}}

+
+
+

Sent by {{brand}} services. If you didn't request this, you can safely ignore this email.

-

{{note}}

-
-

You are receiving this because your address is registered with {{brand}}. If this wasn't you, you can safely ignore it.