Add a one-click confirmation link to registration emails (via the [email] confirm_url and the gRPC Confirm RPC)
All checks were successful
CI / check (push) Successful in 5m27s
All checks were successful
CI / check (push) Successful in 5m27s
This commit is contained in:
parent
b500698e82
commit
66eb0ea243
13 changed files with 121 additions and 18 deletions
|
|
@ -23,20 +23,44 @@ fn brand_mark(brand: &str, accent: &str, logo: &str) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(brand: &str, accent: &str, logo: &str, title: &str, message: &str, code: &str, note: &str) -> String {
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn render(brand: &str, accent: &str, logo: &str, title: &str, message: &str, code: &str, note: &str, button: &str) -> String {
|
||||||
// `message` carries the account name (attacker-influenceable). Substitute every
|
// `message` carries the account name (attacker-influenceable). Substitute every
|
||||||
// OTHER slot first and `{{message}}` LAST, so a message that literally contains
|
// OTHER slot first and `{{message}}` LAST, so a message that literally contains
|
||||||
// `{{code}}`/`{{note}}` (an account named that) is inserted verbatim rather than
|
// `{{code}}`/`{{note}}` (an account named that) is inserted verbatim rather than
|
||||||
// splicing in the real code.
|
// splicing in the real code. `button` is trusted markup built below, not escaped.
|
||||||
BASE.replace("{{brand_mark}}", &brand_mark(brand, accent, logo))
|
BASE.replace("{{brand_mark}}", &brand_mark(brand, accent, logo))
|
||||||
.replace("{{brand}}", &escape(brand))
|
.replace("{{brand}}", &escape(brand))
|
||||||
.replace("{{accent}}", &escape(accent))
|
.replace("{{accent}}", &escape(accent))
|
||||||
.replace("{{title}}", &escape(title))
|
.replace("{{title}}", &escape(title))
|
||||||
.replace("{{code}}", &escape(code))
|
.replace("{{code}}", &escape(code))
|
||||||
.replace("{{note}}", &escape(note))
|
.replace("{{note}}", &escape(note))
|
||||||
|
.replace("{{button}}", button)
|
||||||
.replace("{{message}}", &escape(message))
|
.replace("{{message}}", &escape(message))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Percent-encode a URL query value (RFC 3986 unreserved set passes through).
|
||||||
|
fn percent_encode(s: &str) -> String {
|
||||||
|
let mut out = String::with_capacity(s.len());
|
||||||
|
for b in s.bytes() {
|
||||||
|
match b {
|
||||||
|
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => out.push(b as char),
|
||||||
|
_ => out.push_str(&format!("%{b:02X}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// A one-click call-to-action button for the HTML email.
|
||||||
|
fn action_button(accent: &str, url: &str, label: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"<tr><td align=\"center\" style=\"padding:22px 36px 0;\"><a href=\"{}\" style=\"display:inline-block;background:{};color:#ffffff;font-size:15px;font-weight:700;text-decoration:none;padding:14px 34px;border-radius:11px;\">{}</a></td></tr>",
|
||||||
|
escape(url),
|
||||||
|
escape(accent),
|
||||||
|
escape(label),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn reset(brand: &str, accent: &str, logo: &str, account: &str, code: &str, lang: &str) -> Mail {
|
pub fn reset(brand: &str, accent: &str, logo: &str, account: &str, code: &str, lang: &str) -> Mail {
|
||||||
let acc = || vec![("account", account.to_string())];
|
let acc = || vec![("account", account.to_string())];
|
||||||
Mail {
|
Mail {
|
||||||
|
|
@ -54,19 +78,36 @@ pub fn reset(brand: &str, accent: &str, logo: &str, account: &str, code: &str, l
|
||||||
&crate::render(lang, "Use this code to reset the password for your account {account}.", &acc()),
|
&crate::render(lang, "Use this code to reset the password for your account {account}.", &acc()),
|
||||||
code,
|
code,
|
||||||
&crate::render(lang, "This code expires in 15 minutes. If you didn't ask to reset it, ignore this email.", &[]),
|
&crate::render(lang, "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, lang: &str) -> Mail {
|
pub fn confirm(brand: &str, accent: &str, logo: &str, account: &str, code: &str, confirm_url: &str, lang: &str) -> Mail {
|
||||||
let acc = || vec![("account", account.to_string())];
|
let acc = || vec![("account", account.to_string())];
|
||||||
|
// A one-click confirm link when a web endpoint is configured, else code-only.
|
||||||
|
let link = if confirm_url.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!("{}?account={}&code={}", confirm_url.trim_end_matches('/'), percent_encode(account), percent_encode(code))
|
||||||
|
};
|
||||||
|
let mut text = crate::render(
|
||||||
|
lang,
|
||||||
|
"Confirm your account {account} with:\n /msg NickServ CONFIRM {code}\nThe code expires in 15 minutes.\n",
|
||||||
|
&[("account", account.to_string()), ("code", code.to_string())],
|
||||||
|
);
|
||||||
|
if !link.is_empty() {
|
||||||
|
text.push_str(&crate::render(lang, "Or confirm in one click: {link}", &[("link", link.clone())]));
|
||||||
|
text.push('\n');
|
||||||
|
}
|
||||||
|
let button = if link.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
action_button(accent, &link, &crate::render(lang, "Confirm now", &[]))
|
||||||
|
};
|
||||||
Mail {
|
Mail {
|
||||||
subject: crate::render(lang, "Confirm your {account} registration", &acc()),
|
subject: crate::render(lang, "Confirm your {account} registration", &acc()),
|
||||||
text: crate::render(
|
text,
|
||||||
lang,
|
|
||||||
"Confirm your account {account} with:\n /msg NickServ CONFIRM {code}\nThe code expires in 15 minutes.\n",
|
|
||||||
&[("account", account.to_string()), ("code", code.to_string())],
|
|
||||||
),
|
|
||||||
html: render(
|
html: render(
|
||||||
brand,
|
brand,
|
||||||
accent,
|
accent,
|
||||||
|
|
@ -75,6 +116,7 @@ pub fn confirm(brand: &str, accent: &str, logo: &str, account: &str, code: &str,
|
||||||
&crate::render(lang, "Welcome! Confirm the email for your account {account} with the code below.", &acc()),
|
&crate::render(lang, "Welcome! Confirm the email for your account {account} with the code below.", &acc()),
|
||||||
code,
|
code,
|
||||||
&crate::render(lang, "This code expires in 15 minutes.", &[]),
|
&crate::render(lang, "This code expires in 15 minutes.", &[]),
|
||||||
|
&button,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -130,6 +172,7 @@ pub fn expiry_warning(brand: &str, accent: &str, logo: &str, kind: ExpiryTarget,
|
||||||
&crate::render(lang, "Your {word} {name} has been inactive and will expire in {remaining}.", &base()),
|
&crate::render(lang, "Your {word} {name} has been inactive and will expire in {remaining}.", &base()),
|
||||||
remaining,
|
remaining,
|
||||||
&keep,
|
&keep,
|
||||||
|
"",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -139,3 +182,32 @@ pub fn expiry_warning(brand: &str, accent: &str, logo: &str, kind: ExpiryTarget,
|
||||||
fn escape(s: &str) -> String {
|
fn escape(s: &str) -> String {
|
||||||
s.replace('&', "&").replace('<', "<").replace('>', ">")
|
s.replace('&', "&").replace('<', "<").replace('>', ">")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn confirm_link_present_only_when_url_set() {
|
||||||
|
let with = confirm("Brand", "#000000", "", "alice", "ABC123", "https://x.net/confirm", "en");
|
||||||
|
assert!(with.text.contains("https://x.net/confirm?account=alice&code=ABC123"), "text link: {}", with.text);
|
||||||
|
assert!(with.html.contains("href=\"https://x.net/confirm?account=alice&code=ABC123\""), "html button href escaped");
|
||||||
|
assert!(with.html.contains("Confirm now"), "button label");
|
||||||
|
|
||||||
|
let none = confirm("Brand", "#000000", "", "alice", "ABC123", "", "en");
|
||||||
|
assert!(!none.text.contains("one click"), "no link line: {}", none.text);
|
||||||
|
assert!(!none.html.contains("Confirm now"), "no button");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn confirm_link_percent_encodes_the_account() {
|
||||||
|
let m = confirm("B", "#000000", "", "a[b", "K", "https://x/confirm", "en");
|
||||||
|
assert!(m.text.contains("account=a%5Bb&code=K"), "encoded account: {}", m.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn confirm_url_trailing_slash_is_trimmed() {
|
||||||
|
let m = confirm("B", "#000000", "", "z", "K", "https://x/confirm/", "en");
|
||||||
|
assert!(m.text.contains("https://x/confirm?account=z"), "trimmed: {}", m.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@
|
||||||
</table>
|
</table>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
|
|
||||||
|
{{button}}
|
||||||
|
|
||||||
<tr><td align="center" style="padding:20px 36px 4px;">
|
<tr><td align="center" style="padding:20px 36px 4px;">
|
||||||
<div style="display:inline-block;background:{{accent}};color:#ffffff;font-size:14px;font-weight:600;padding:13px 30px;border-radius:11px;box-shadow:0 2px 8px rgba(15,23,42,.14);">📋 Copy code</div>
|
<div style="display:inline-block;background:{{accent}};color:#ffffff;font-size:14px;font-weight:600;padding:13px 30px;border-radius:11px;box-shadow:0 2px 8px rgba(15,23,42,.14);">📋 Copy code</div>
|
||||||
<div style="margin-top:10px;font-size:12px;color:#a4adba;">Tap and hold the code to copy it.</div>
|
<div style="margin-top:10px;font-size:12px;color:#a4adba;">Tap and hold the code to copy it.</div>
|
||||||
|
|
|
||||||
|
|
@ -1363,5 +1363,7 @@
|
||||||
"\u0002{by}\u0002 set you as successor of \u0002{chan}\u0002.": "\u0002{by}\u0002 hat dich als Nachfolger von \u0002{chan}\u0002 festgelegt.",
|
"\u0002{by}\u0002 set you as successor of \u0002{chan}\u0002.": "\u0002{by}\u0002 hat dich als Nachfolger von \u0002{chan}\u0002 festgelegt.",
|
||||||
"Added \u0002{count}\u0002 channel to your auto-join list.": "\u0002{count}\u0002 Kanal zu deiner Auto-Join-Liste hinzugefügt.",
|
"Added \u0002{count}\u0002 channel to your auto-join list.": "\u0002{count}\u0002 Kanal zu deiner Auto-Join-Liste hinzugefügt.",
|
||||||
"Added \u0002{count}\u0002 channels to your auto-join list.": "\u0002{count}\u0002 Kanäle zu deiner Auto-Join-Liste hinzugefügt.",
|
"Added \u0002{count}\u0002 channels to your auto-join list.": "\u0002{count}\u0002 Kanäle zu deiner Auto-Join-Liste hinzugefügt.",
|
||||||
"Unknown AJOIN command \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 or \u0002LIST\u0002.": "Unbekannter AJOIN-Befehl \u0002{other}\u0002. Nutze \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 oder \u0002LIST\u0002."
|
"Unknown AJOIN command \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 or \u0002LIST\u0002.": "Unbekannter AJOIN-Befehl \u0002{other}\u0002. Nutze \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 oder \u0002LIST\u0002.",
|
||||||
|
"Or confirm in one click: {link}": "Oder bestätige mit einem Klick: {link}",
|
||||||
|
"Confirm now": "Jetzt bestätigen"
|
||||||
}
|
}
|
||||||
|
|
@ -1363,5 +1363,7 @@
|
||||||
"\u0002{by}\u0002 set you as successor of \u0002{chan}\u0002.": "\u0002{by}\u0002 te estableció como sucesor de \u0002{chan}\u0002.",
|
"\u0002{by}\u0002 set you as successor of \u0002{chan}\u0002.": "\u0002{by}\u0002 te estableció como sucesor de \u0002{chan}\u0002.",
|
||||||
"Added \u0002{count}\u0002 channel to your auto-join list.": "\u0002{count}\u0002 canal agregado a tu lista de auto-unión.",
|
"Added \u0002{count}\u0002 channel to your auto-join list.": "\u0002{count}\u0002 canal agregado a tu lista de auto-unión.",
|
||||||
"Added \u0002{count}\u0002 channels to your auto-join list.": "\u0002{count}\u0002 canales agregados a tu lista de auto-unión.",
|
"Added \u0002{count}\u0002 channels to your auto-join list.": "\u0002{count}\u0002 canales agregados a tu lista de auto-unión.",
|
||||||
"Unknown AJOIN command \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 or \u0002LIST\u0002.": "Comando AJOIN desconocido \u0002{other}\u0002. Usá \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 o \u0002LIST\u0002."
|
"Unknown AJOIN command \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 or \u0002LIST\u0002.": "Comando AJOIN desconocido \u0002{other}\u0002. Usá \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 o \u0002LIST\u0002.",
|
||||||
|
"Or confirm in one click: {link}": "O confirmá con un clic: {link}",
|
||||||
|
"Confirm now": "Confirmar ahora"
|
||||||
}
|
}
|
||||||
|
|
@ -1363,5 +1363,7 @@
|
||||||
"\u0002{by}\u0002 set you as successor of \u0002{chan}\u0002.": "\u0002{by}\u0002 te estableció como sucesor de \u0002{chan}\u0002.",
|
"\u0002{by}\u0002 set you as successor of \u0002{chan}\u0002.": "\u0002{by}\u0002 te estableció como sucesor de \u0002{chan}\u0002.",
|
||||||
"Added \u0002{count}\u0002 channel to your auto-join list.": "\u0002{count}\u0002 canal añadido a tu lista de auto-unión.",
|
"Added \u0002{count}\u0002 channel to your auto-join list.": "\u0002{count}\u0002 canal añadido a tu lista de auto-unión.",
|
||||||
"Added \u0002{count}\u0002 channels to your auto-join list.": "\u0002{count}\u0002 canales añadidos a tu lista de auto-unión.",
|
"Added \u0002{count}\u0002 channels to your auto-join list.": "\u0002{count}\u0002 canales añadidos a tu lista de auto-unión.",
|
||||||
"Unknown AJOIN command \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 or \u0002LIST\u0002.": "Comando AJOIN desconocido \u0002{other}\u0002. Usa \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 o \u0002LIST\u0002."
|
"Unknown AJOIN command \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 or \u0002LIST\u0002.": "Comando AJOIN desconocido \u0002{other}\u0002. Usa \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 o \u0002LIST\u0002.",
|
||||||
|
"Or confirm in one click: {link}": "O confirma con un clic: {link}",
|
||||||
|
"Confirm now": "Confirmar ahora"
|
||||||
}
|
}
|
||||||
|
|
@ -1363,5 +1363,7 @@
|
||||||
"\u0002{by}\u0002 set you as successor of \u0002{chan}\u0002.": "\u0002{by}\u0002 vous a défini comme successeur de \u0002{chan}\u0002.",
|
"\u0002{by}\u0002 set you as successor of \u0002{chan}\u0002.": "\u0002{by}\u0002 vous a défini comme successeur de \u0002{chan}\u0002.",
|
||||||
"Added \u0002{count}\u0002 channel to your auto-join list.": "\u0002{count}\u0002 salon ajouté à votre liste d'auto-join.",
|
"Added \u0002{count}\u0002 channel to your auto-join list.": "\u0002{count}\u0002 salon ajouté à votre liste d'auto-join.",
|
||||||
"Added \u0002{count}\u0002 channels to your auto-join list.": "\u0002{count}\u0002 salons ajoutés à votre liste d'auto-join.",
|
"Added \u0002{count}\u0002 channels to your auto-join list.": "\u0002{count}\u0002 salons ajoutés à votre liste d'auto-join.",
|
||||||
"Unknown AJOIN command \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 or \u0002LIST\u0002.": "Commande AJOIN inconnue \u0002{other}\u0002. Utilisez \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 ou \u0002LIST\u0002."
|
"Unknown AJOIN command \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 or \u0002LIST\u0002.": "Commande AJOIN inconnue \u0002{other}\u0002. Utilisez \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 ou \u0002LIST\u0002.",
|
||||||
|
"Or confirm in one click: {link}": "Ou confirmez en un clic : {link}",
|
||||||
|
"Confirm now": "Confirmer maintenant"
|
||||||
}
|
}
|
||||||
|
|
@ -1363,5 +1363,7 @@
|
||||||
"\u0002{by}\u0002 set you as successor of \u0002{chan}\u0002.": "\u0002{by}\u0002 definiu você como sucessor de \u0002{chan}\u0002.",
|
"\u0002{by}\u0002 set you as successor of \u0002{chan}\u0002.": "\u0002{by}\u0002 definiu você como sucessor de \u0002{chan}\u0002.",
|
||||||
"Added \u0002{count}\u0002 channel to your auto-join list.": "\u0002{count}\u0002 canal adicionado à sua lista de auto-entrada.",
|
"Added \u0002{count}\u0002 channel to your auto-join list.": "\u0002{count}\u0002 canal adicionado à sua lista de auto-entrada.",
|
||||||
"Added \u0002{count}\u0002 channels to your auto-join list.": "\u0002{count}\u0002 canais adicionados à sua lista de auto-entrada.",
|
"Added \u0002{count}\u0002 channels to your auto-join list.": "\u0002{count}\u0002 canais adicionados à sua lista de auto-entrada.",
|
||||||
"Unknown AJOIN command \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 or \u0002LIST\u0002.": "Comando AJOIN desconhecido \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 ou \u0002LIST\u0002."
|
"Unknown AJOIN command \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 or \u0002LIST\u0002.": "Comando AJOIN desconhecido \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 ou \u0002LIST\u0002.",
|
||||||
|
"Or confirm in one click: {link}": "Ou confirme com um clique: {link}",
|
||||||
|
"Confirm now": "Confirmar agora"
|
||||||
}
|
}
|
||||||
|
|
@ -1363,5 +1363,7 @@
|
||||||
"\u0002{by}\u0002 set you as successor of \u0002{chan}\u0002.": "\u0002{by}\u0002 definiu-o como sucessor de \u0002{chan}\u0002.",
|
"\u0002{by}\u0002 set you as successor of \u0002{chan}\u0002.": "\u0002{by}\u0002 definiu-o como sucessor de \u0002{chan}\u0002.",
|
||||||
"Added \u0002{count}\u0002 channel to your auto-join list.": "\u0002{count}\u0002 canal adicionado à sua lista de auto-entrada.",
|
"Added \u0002{count}\u0002 channel to your auto-join list.": "\u0002{count}\u0002 canal adicionado à sua lista de auto-entrada.",
|
||||||
"Added \u0002{count}\u0002 channels to your auto-join list.": "\u0002{count}\u0002 canais adicionados à sua lista de auto-entrada.",
|
"Added \u0002{count}\u0002 channels to your auto-join list.": "\u0002{count}\u0002 canais adicionados à sua lista de auto-entrada.",
|
||||||
"Unknown AJOIN command \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 or \u0002LIST\u0002.": "Comando AJOIN desconhecido \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 ou \u0002LIST\u0002."
|
"Unknown AJOIN command \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 or \u0002LIST\u0002.": "Comando AJOIN desconhecido \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 ou \u0002LIST\u0002.",
|
||||||
|
"Or confirm in one click: {link}": "Ou confirme com um clique: {link}",
|
||||||
|
"Confirm now": "Confirmar agora"
|
||||||
}
|
}
|
||||||
|
|
@ -332,6 +332,12 @@ pub struct Email {
|
||||||
// email clients don't render inline SVG or data URIs).
|
// email clients don't render inline SVG or data URIs).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub logo: String,
|
pub logo: String,
|
||||||
|
// Optional base URL of a web endpoint that confirms an account from a code,
|
||||||
|
// e.g. "https://example.net/confirm". When set, confirmation emails include a
|
||||||
|
// one-click link (`<url>?account=<name>&code=<code>`) alongside the CONFIRM
|
||||||
|
// command; the endpoint calls the gRPC Confirm RPC.
|
||||||
|
#[serde(default)]
|
||||||
|
pub confirm_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_brand() -> String {
|
fn default_brand() -> String {
|
||||||
|
|
|
||||||
|
|
@ -247,6 +247,15 @@ impl Db {
|
||||||
&self.email_logo
|
&self.email_logo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Base URL of the web confirm endpoint (empty = no one-click link in emails).
|
||||||
|
pub fn email_confirm_url(&self) -> &str {
|
||||||
|
&self.email_confirm_url
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_email_confirm_url(&mut self, url: &str) {
|
||||||
|
self.email_confirm_url = url.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_email_brand(&mut self, brand: &str) {
|
pub fn set_email_brand(&mut self, brand: &str) {
|
||||||
self.email_brand = brand.to_string();
|
self.email_brand = brand.to_string();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1144,6 +1144,7 @@ pub struct Db {
|
||||||
email_brand: String,
|
email_brand: String,
|
||||||
email_accent: String,
|
email_accent: String,
|
||||||
email_logo: String,
|
email_logo: String,
|
||||||
|
email_confirm_url: String,
|
||||||
// Node-local, non-persisted email codes, keyed by account.
|
// Node-local, non-persisted email codes, keyed by account.
|
||||||
codes: HashMap<String, PendingCode>,
|
codes: HashMap<String, PendingCode>,
|
||||||
// Node-local, non-persisted brute-force throttle for password authentication.
|
// Node-local, non-persisted brute-force throttle for password authentication.
|
||||||
|
|
@ -1239,7 +1240,7 @@ impl Db {
|
||||||
apply(&mut accounts, &mut channels, &mut grouped, &mut bots, &mut host_cfg, &mut net, event);
|
apply(&mut accounts, &mut channels, &mut grouped, &mut bots, &mut host_cfg, &mut net, event);
|
||||||
}
|
}
|
||||||
tracing::info!(accounts = accounts.len(), channels = channels.len(), "account store loaded");
|
tracing::info!(accounts = accounts.len(), channels = channels.len(), "account store loaded");
|
||||||
Self { accounts, channels, grouped, log, extban_enabled: None, notify_exclude: Vec::new(), live_extbans: None, live_chanmodes: None, 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(), auth_fails: HashMap::new(), vhost_req_times: HashMap::new(), report_times: HashMap::new(), bots, host_cfg, net, ignores: Vec::new(), defcon: 5, external_accounts: false, confusable_check: true, default_language: "en".to_string(), available_languages: vec!["en".to_string()] }
|
Self { accounts, channels, grouped, log, extban_enabled: None, notify_exclude: Vec::new(), live_extbans: None, live_chanmodes: None, scram_iterations: scram::DEFAULT_ITERATIONS, email_enabled: false, email_brand: "Network Services".to_string(), email_accent: "#4f46e5".to_string(), email_logo: String::new(), email_confirm_url: String::new(), codes: HashMap::new(), auth_fails: HashMap::new(), vhost_req_times: HashMap::new(), report_times: HashMap::new(), bots, host_cfg, net, ignores: Vec::new(), defcon: 5, external_accounts: false, confusable_check: true, default_language: "en".to_string(), available_languages: vec!["en".to_string()] }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fold an entry authored by another node into the store — the services-side
|
/// Fold an entry authored by another node into the store — the services-side
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ impl Engine {
|
||||||
if status == AuthorityStatus::Ok && !self.db.is_verified(name) {
|
if status == AuthorityStatus::Ok && !self.db.is_verified(name) {
|
||||||
if let Some(addr) = addr {
|
if let Some(addr) = addr {
|
||||||
let code = self.db.issue_code(name, db::CodeKind::Confirm);
|
let code = self.db.issue_code(name, db::CodeKind::Confirm);
|
||||||
let mail = echo_api::email::confirm(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), name, &code, &self.lang_for_account(name));
|
let mail = echo_api::email::confirm(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), name, &code, self.db.email_confirm_url(), &self.lang_for_account(name));
|
||||||
self.emit_irc(NetAction::SendEmail { to: addr, subject: mail.subject, text: mail.text, html: Some(mail.html) });
|
self.emit_irc(NetAction::SendEmail { to: addr, subject: mail.subject, text: mail.text, html: Some(mail.html) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -279,7 +279,7 @@ impl Engine {
|
||||||
}
|
}
|
||||||
Some((false, Some(addr))) => {
|
Some((false, Some(addr))) => {
|
||||||
let code = self.db.issue_code(&account, db::CodeKind::Confirm);
|
let code = self.db.issue_code(&account, db::CodeKind::Confirm);
|
||||||
let mail = echo_api::email::confirm(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), &account, &code, &self.lang_for_account(&account));
|
let mail = echo_api::email::confirm(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), &account, &code, self.db.email_confirm_url(), &self.lang_for_account(&account));
|
||||||
let mut out = resp("verification_required", "VERIFICATION_REQUIRED", "A new confirmation code has been emailed.");
|
let mut out = resp("verification_required", "VERIFICATION_REQUIRED", "A new confirmation code has been emailed.");
|
||||||
out.push(NetAction::SendEmail { to: addr, subject: mail.subject, text: mail.text, html: Some(mail.html) });
|
out.push(NetAction::SendEmail { to: addr, subject: mail.subject, text: mail.text, html: Some(mail.html) });
|
||||||
out
|
out
|
||||||
|
|
@ -344,7 +344,7 @@ impl Engine {
|
||||||
if needs_verify {
|
if needs_verify {
|
||||||
if let Some(addr) = addr {
|
if let Some(addr) = addr {
|
||||||
let code = self.db.issue_code(account, db::CodeKind::Confirm);
|
let code = self.db.issue_code(account, db::CodeKind::Confirm);
|
||||||
let mail = echo_api::email::confirm(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), account, &code, &self.lang_for_account(account));
|
let mail = echo_api::email::confirm(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), account, &code, self.db.email_confirm_url(), &self.lang_for_account(account));
|
||||||
out.push(NetAction::SendEmail { to: addr, subject: mail.subject, text: mail.text, html: Some(mail.html) });
|
out.push(NetAction::SendEmail { to: addr, subject: mail.subject, text: mail.text, html: Some(mail.html) });
|
||||||
if let RegReply::NickServ { agent, uid, .. } = &reply {
|
if let RegReply::NickServ { agent, uid, .. } = &reply {
|
||||||
out.push(NetAction::Notice { from: agent.clone(), to: uid.clone(), text: echo_api::render(&self.lang_for_account(account), "A confirmation code has been emailed to you. Confirm with \x02CONFIRM <code>\x02.", &[]) });
|
out.push(NetAction::Notice { from: agent.clone(), to: uid.clone(), text: echo_api::render(&self.lang_for_account(account), "A confirmation code has been emailed to you. Confirm with \x02CONFIRM <code>\x02.", &[]) });
|
||||||
|
|
|
||||||
|
|
@ -281,6 +281,7 @@ async fn main() -> Result<()> {
|
||||||
db.set_email_brand(&email.brand);
|
db.set_email_brand(&email.brand);
|
||||||
db.set_email_accent(&email.accent);
|
db.set_email_accent(&email.accent);
|
||||||
db.set_email_logo(&email.logo);
|
db.set_email_logo(&email.logo);
|
||||||
|
db.set_email_confirm_url(&email.confirm_url);
|
||||||
}
|
}
|
||||||
let engine = Arc::new(Mutex::new(Engine::new(services, db)));
|
let engine = Arc::new(Mutex::new(Engine::new(services, db)));
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue