// Email content: renders the HTML templates in ../templates/email and pairs each
// with a plaintext fallback. The link layer sends both as multipart/alternative.
pub struct Mail {
pub subject: String,
pub text: String,
pub html: String,
}
const BASE: &str = include_str!("../templates/email/base.html");
// 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))
.replace("{{code}}", &escape(code))
.replace("{{note}}", &escape(note))
}
pub fn reset(brand: &str, accent: &str, logo: &str, account: &str, code: &str) -> Mail {
Mail {
subject: format!("Password reset for {account}"),
text: format!(
"Your password reset code for {account} is: {code}\nIt expires in 15 minutes.\nReset with:\n /msg NickServ RESETPASS {account} {code} \n"
),
html: render(
brand,
accent,
logo,
"Password reset",
&format!("Use this code to reset the password for your account {account}."),
code,
"This code expires in 15 minutes. If you didn't ask to reset it, ignore this email.",
),
}
}
pub fn confirm(brand: &str, accent: &str, logo: &str, account: &str, code: &str) -> Mail {
Mail {
subject: format!("Confirm your {account} registration"),
text: format!(
"Confirm your account {account} with:\n /msg NickServ CONFIRM {code}\nThe code expires in 15 minutes.\n"
),
html: render(
brand,
accent,
logo,
"Confirm your account",
&format!("Welcome! Confirm the email for your account {account} with the code below."),
code,
"This code expires in 15 minutes.",
),
}
}
// Warn the owner of an account or channel that inactivity will soon expire it.
// `kind` is "account" or "channel", `remaining` a human span ("7 days"). The
// remaining span takes the prominent code slot.
pub fn expiry_warning(brand: &str, accent: &str, logo: &str, kind: &str, name: &str, remaining: &str) -> Mail {
let keep = if kind == "channel" {
"To keep it, have a member join the channel before then. Otherwise it will be removed."
} else {
"To keep it, just identify to it before then. Otherwise it will be removed."
};
Mail {
subject: format!("Your {kind} {name} is about to expire"),
text: format!(
"Your {kind} {name} has been inactive and will expire in {remaining}.\n{keep}\n"
),
html: render(
brand,
accent,
logo,
"About to expire",
&format!("Your {kind} {name} has been inactive and will expire in {remaining}."),
remaining,
keep,
),
}
}
// Escape the few characters that matter inside HTML text so a value can't break
// out of the template.
fn escape(s: &str) -> String {
s.replace('&', "&").replace('<', "<").replace('>', ">")
}