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

@ -19,11 +19,8 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return;
};
let code = db.issue_code(&canonical, CodeKind::Reset);
ctx.send_email(
email,
format!("Password reset for {canonical}"),
format!("Your password reset code for {canonical} is: {code}\nIt expires in 15 minutes. Reset with:\n /msg NickServ RESETPASS {canonical} {code} <newpassword>"),
);
let mail = crate::email::reset(db.email_brand(), &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."));
}
(Some(&name), Some(&code), Some(&newpass)) => {

View file

@ -71,9 +71,9 @@ pub enum NetAction {
// Internal only: a password change awaiting the same off-thread derivation.
// The link layer derives, then calls Engine::complete_password_change.
DeferPassword { account: String, password: String, agent: String, uid: String },
// Internal only: send an email. The link layer pipes it to the configured
// mail command off-thread; never serialized to the ircd.
SendEmail { to: String, subject: String, body: String },
// Internal only: send an email (plaintext + optional HTML). The link layer
// pipes it to the configured mail command off-thread; never serialized.
SendEmail { to: String, subject: String, text: String, html: Option<String> },
}
// How to answer a registration once its credentials have been derived.