mirror of
https://github.com/stoatchat/stoatchat.git
synced 2026-07-14 21:47:02 +00:00
Co-authored-by: izzy <me@insrt.uk> Signed-off-by: Zomatree <me@zomatree.live> Signed-off-by: izzy <me@insrt.uk>
272 lines
8.8 KiB
Rust
272 lines
8.8 KiB
Rust
use std::{collections::HashSet, sync::LazyLock};
|
|
|
|
use lettre::{
|
|
transport::smtp::{authentication::Credentials, client::Tls},
|
|
SmtpTransport,
|
|
};
|
|
use regex::Regex;
|
|
use revolt_config::{config, ApiSmtp};
|
|
use revolt_result::Result;
|
|
|
|
static SPLIT: LazyLock<Regex> = LazyLock::new(|| Regex::new("([^@]+)(@.+)").unwrap());
|
|
static SYMBOL_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new("\\+.+|\\.").unwrap());
|
|
static HANDLEBARS: LazyLock<handlebars::Handlebars<'static>> =
|
|
LazyLock::new(handlebars::Handlebars::new);
|
|
static REVOLT_SOURCE_LIST: LazyLock<HashSet<String>> = LazyLock::new(|| {
|
|
include_str!("../../assets/revolt_source_list.txt")
|
|
.split('\n')
|
|
.map(|x| x.into())
|
|
.collect()
|
|
});
|
|
|
|
/// Strip special characters and aliases from emails
|
|
pub fn normalise_email(original: String) -> String {
|
|
let split = SPLIT.captures(&original).unwrap();
|
|
let mut clean = SYMBOL_RE
|
|
.replace_all(split.get(1).unwrap().as_str(), "")
|
|
.to_string();
|
|
|
|
clean.push_str(split.get(2).unwrap().as_str());
|
|
clean.to_lowercase()
|
|
}
|
|
|
|
/// Email template
|
|
#[derive(Clone)]
|
|
pub struct Template {
|
|
/// Title of the email
|
|
pub title: String,
|
|
/// Plain text version of this email
|
|
pub text: String,
|
|
/// HTML version of this email
|
|
pub html: Option<String>,
|
|
/// URL to redirect people to from the email
|
|
///
|
|
/// Use `{{url}}` to fill this field.
|
|
///
|
|
/// Any given URL will be suffixed with a unique token if applicable.
|
|
///
|
|
/// e.g. `https://example.com?t=` becomes `https://example.com?t=UNIQUE_CODE`
|
|
pub url: String,
|
|
}
|
|
|
|
/// Email templates
|
|
#[derive(Clone)]
|
|
pub struct Templates {
|
|
/// Template for email verification
|
|
pub verify: Template,
|
|
/// Template for password reset
|
|
pub reset: Template,
|
|
/// Template for password reset when the account already exists on creation
|
|
pub reset_existing: Template,
|
|
/// Template for account deletion
|
|
pub deletion: Template,
|
|
/// Template for suspention
|
|
pub suspension: Template,
|
|
}
|
|
|
|
pub async fn email_templates() -> Templates {
|
|
let config = config().await;
|
|
|
|
if std::env::var("TEST_DB").is_ok() {
|
|
Templates {
|
|
verify: Template {
|
|
title: "verify".into(),
|
|
text: "[[{{url}}]]".into(),
|
|
url: "".into(),
|
|
html: None,
|
|
},
|
|
reset: Template {
|
|
title: "reset".into(),
|
|
text: "[[{{url}}]]".into(),
|
|
url: "".into(),
|
|
html: None,
|
|
},
|
|
reset_existing: Template {
|
|
title: "reset_existing".into(),
|
|
text: "[[{{url}}]]".into(),
|
|
url: "".into(),
|
|
html: None,
|
|
},
|
|
deletion: Template {
|
|
title: "deletion".into(),
|
|
text: "[[{{url}}]]".into(),
|
|
url: "".into(),
|
|
html: None,
|
|
},
|
|
suspension: Template {
|
|
title: "suspension".into(),
|
|
text: "[[dummy]]".into(),
|
|
url: "".into(),
|
|
html: None,
|
|
},
|
|
}
|
|
} else if config.production {
|
|
Templates {
|
|
verify: Template {
|
|
title: "Verify your Stoat account.".into(),
|
|
text: include_str!("../../templates/verify.txt").into(),
|
|
url: format!("{}/login/verify/", config.hosts.app),
|
|
html: Some(include_str!("../../templates/verify.html").into()),
|
|
},
|
|
reset: Template {
|
|
title: "Reset your Stoat password.".into(),
|
|
text: include_str!("../../templates/reset.txt").into(),
|
|
url: format!("{}/login/reset/", config.hosts.app),
|
|
html: Some(include_str!("../../templates/reset.html").into()),
|
|
},
|
|
reset_existing: Template {
|
|
title: "You already have a Stoat account, reset your password.".into(),
|
|
text: include_str!("../../templates/reset-existing.txt").into(),
|
|
url: format!("{}/login/reset/", config.hosts.app),
|
|
html: Some(include_str!("../../templates/reset-existing.html").into()),
|
|
},
|
|
deletion: Template {
|
|
title: "Confirm account deletion.".into(),
|
|
text: include_str!("../../templates/deletion.txt").into(),
|
|
url: format!("{}/delete/", config.hosts.app),
|
|
html: Some(include_str!("../../templates/deletion.html").into()),
|
|
},
|
|
suspension: Template {
|
|
title: "Account Suspension".to_string(),
|
|
html: Some(include_str!("../../templates/suspension.html").to_owned()),
|
|
text: include_str!("../../templates/suspension.txt").to_owned(),
|
|
url: Default::default(),
|
|
},
|
|
}
|
|
} else {
|
|
Templates {
|
|
verify: Template {
|
|
title: "Verify your account.".into(),
|
|
text: include_str!("../../templates/verify.whitelabel.txt").into(),
|
|
url: format!("{}/login/verify/", config.hosts.app),
|
|
html: None,
|
|
},
|
|
reset: Template {
|
|
title: "Reset your password.".into(),
|
|
text: include_str!("../../templates/reset.whitelabel.txt").into(),
|
|
url: format!("{}/login/reset/", config.hosts.app),
|
|
html: None,
|
|
},
|
|
reset_existing: Template {
|
|
title: "Reset your password.".into(),
|
|
text: include_str!("../../templates/reset.whitelabel.txt").into(),
|
|
url: format!("{}/login/reset/", config.hosts.app),
|
|
html: None,
|
|
},
|
|
deletion: Template {
|
|
title: "Confirm account deletion.".into(),
|
|
text: include_str!("../../templates/deletion.whitelabel.txt").into(),
|
|
url: format!("{}/delete/", config.hosts.app),
|
|
html: None,
|
|
},
|
|
suspension: Template {
|
|
title: "Account Suspension".to_string(),
|
|
text: include_str!("../../templates/suspension.whitelabel.txt").to_owned(),
|
|
url: Default::default(),
|
|
html: None,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Create SMTP transport
|
|
pub fn create_transport(smtp: &ApiSmtp) -> SmtpTransport {
|
|
let relay = if smtp.use_starttls == Some(true) {
|
|
SmtpTransport::starttls_relay(&smtp.host).unwrap()
|
|
} else {
|
|
SmtpTransport::relay(&smtp.host).unwrap()
|
|
};
|
|
|
|
let relay = if let Some(port) = smtp.port {
|
|
relay.port(port.try_into().unwrap())
|
|
} else {
|
|
relay
|
|
};
|
|
|
|
let relay = if smtp.use_tls == Some(false) {
|
|
relay.tls(Tls::None)
|
|
} else {
|
|
relay
|
|
};
|
|
|
|
relay
|
|
.credentials(Credentials::new(
|
|
smtp.username.clone(),
|
|
smtp.password.clone(),
|
|
))
|
|
.build()
|
|
}
|
|
|
|
/// Render an email template
|
|
fn render_template(text: &str, variables: &handlebars::JsonValue) -> Result<String> {
|
|
HANDLEBARS
|
|
.render_template(text, variables)
|
|
.map_err(|_| create_error!(RenderFail))
|
|
}
|
|
|
|
/// Send an email
|
|
pub fn send_email(
|
|
smtp: &ApiSmtp,
|
|
address: String,
|
|
template: &Template,
|
|
variables: handlebars::JsonValue,
|
|
) -> Result<()> {
|
|
let m = lettre::Message::builder()
|
|
.from(smtp.from_address.parse().expect("valid `smtp_from`"))
|
|
.to(address.parse().expect("valid `smtp_to`"))
|
|
.subject(template.title.clone());
|
|
|
|
let m = if let Some(reply_to) = &smtp.reply_to {
|
|
m.reply_to(reply_to.parse().expect("valid `smtp_reply_to`"))
|
|
} else {
|
|
m
|
|
};
|
|
|
|
let text = render_template(&template.text, &variables).expect("valid `template`");
|
|
|
|
let m = if let Some(html) = &template.html {
|
|
m.multipart(lettre::message::MultiPart::alternative_plain_html(
|
|
text,
|
|
render_template(html, &variables).expect("valid `template`"),
|
|
))
|
|
} else {
|
|
m.body(text)
|
|
}
|
|
.expect("valid `message`");
|
|
|
|
use lettre::Transport;
|
|
let sender = create_transport(smtp);
|
|
|
|
match sender.send(&m) {
|
|
Ok(_) => Ok(()),
|
|
Err(error) => {
|
|
error!(
|
|
"Failed to send email to {}!\nlettre error: {}",
|
|
address, error
|
|
);
|
|
|
|
revolt_config::capture_error(&error);
|
|
|
|
Err(create_error!(EmailFailed))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn validate_email(email: &str) -> Result<()> {
|
|
// Make sure this is an actual email
|
|
if !validator::validate_email(email) {
|
|
return Err(create_error!(IncorrectData {
|
|
with: "email".to_string()
|
|
}));
|
|
}
|
|
|
|
// Check if the email is blacklisted
|
|
if let Some(domain) = email.split('@').next_back() {
|
|
if REVOLT_SOURCE_LIST.contains(&domain.to_string()) {
|
|
return Err(create_error!(Blacklisted));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|