[DO NOT MERGE] Email notification system (#4338)

* Migration

* Fixup db models

* Redis

* Stuff

* Switch PKs to BIGSERIALs, insert to notifications_deliveries when inserting notifications

* Queue, templates

* Query cache

* Fixes, fixtures

* Perf, cache template data & HTML bodies

* Notification type configuration, ResetPassword notification type

* Reset password

* Query cache

* Clippy + fmt

* Traces, fix typo, fix user email in ResetPassword

* send_email

* Models, db

* Remove dead code, adjust notification settings in migration

* Clippy fmt

* Delete dead code, fixes

* Fmt

* Update apps/labrinth/src/queue/email.rs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com>

* Remove old fixtures

* Unify email retry delay

* Fix type

* External notifications

* Remove `notifications_types_preference_restrictions`, as user notification preferences is out of scope for this PR

* Query cache, fmt, clippy

* Fix join in get_many_user_exposed_on_site

* Remove migration comment

* Query cache

* Update html body urls

* Remove comment

* Add paymentfailed.service variable to PaymentFailed notification variant

* Fix compile error

* Fix deleting notifications

* Update apps/labrinth/src/database/models/user_item.rs

Co-authored-by: Josiah Glosson <soujournme@gmail.com>
Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com>

* Update apps/labrinth/src/database/models/user_item.rs

Co-authored-by: Josiah Glosson <soujournme@gmail.com>
Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com>

* Update Cargo.toml

Co-authored-by: Josiah Glosson <soujournme@gmail.com>
Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com>

* Update apps/labrinth/migrations/20250902133943_notification-extension.sql

Co-authored-by: Josiah Glosson <soujournme@gmail.com>
Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com>

* Address review comments

* Fix compliation

* Update apps/labrinth/src/database/models/users_notifications_preferences_item.rs

Co-authored-by: Josiah Glosson <soujournme@gmail.com>
Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com>

* Use strfmt to format emails

* Configurable Reply-To

* Configurable Reply-To

* Refactor for email background task

* Send some emails inline

* Fix account creation email check

* Revert "Use strfmt to format emails"

This reverts commit e0d6614afe51fa6349918377e953ba294c34ae0b.

* Reintroduce fill_template

* Set password reset email inline

* Process more emails per index

* clippy fmt

* Query cache

---------

Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Josiah Glosson <soujournme@gmail.com>
This commit is contained in:
François-Xavier Talbot
2025-09-15 15:02:29 -04:00
committed by GitHub
parent 1491642209
commit 902d749293
51 changed files with 2958 additions and 3652 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,95 +0,0 @@
use lettre::message::Mailbox;
use lettre::message::header::ContentType;
use lettre::transport::smtp::authentication::Credentials;
use lettre::transport::smtp::client::{Tls, TlsParameters};
use lettre::{Message, SmtpTransport, Transport};
use thiserror::Error;
use tracing::warn;
#[derive(Error, Debug)]
pub enum MailError {
#[error("Environment Error")]
Env(#[from] dotenvy::Error),
#[error("Mail Error: {0}")]
Mail(#[from] lettre::error::Error),
#[error("Address Parse Error: {0}")]
Address(#[from] lettre::address::AddressError),
#[error("SMTP Error: {0}")]
Smtp(#[from] lettre::transport::smtp::Error),
}
pub fn send_email_raw(
to: String,
subject: String,
body: String,
) -> Result<(), MailError> {
let from_name = dotenvy::var("SMTP_FROM_NAME")
.unwrap_or_else(|_| "Modrinth".to_string());
let from_address = dotenvy::var("SMTP_FROM_ADDRESS")
.unwrap_or_else(|_| "no-reply@mail.modrinth.com".to_string());
let email = Message::builder()
.from(Mailbox::new(Some(from_name), from_address.parse()?))
.to(to.parse()?)
.subject(subject)
.header(ContentType::TEXT_HTML)
.body(body)?;
let username = dotenvy::var("SMTP_USERNAME")?;
let password = dotenvy::var("SMTP_PASSWORD")?;
let host = dotenvy::var("SMTP_HOST")?;
let port = dotenvy::var("SMTP_PORT")?.parse::<u16>().unwrap_or(465);
let creds =
(!username.is_empty()).then(|| Credentials::new(username, password));
let tls_setting = match dotenvy::var("SMTP_TLS")?.as_str() {
"none" => Tls::None,
"opportunistic_start_tls" => {
Tls::Opportunistic(TlsParameters::new(host.to_string())?)
}
"requires_start_tls" => {
Tls::Required(TlsParameters::new(host.to_string())?)
}
"tls" => Tls::Wrapper(TlsParameters::new(host.to_string())?),
_ => {
warn!("Unrecognized SMTP TLS setting. Defaulting to TLS.");
Tls::Wrapper(TlsParameters::new(host.to_string())?)
}
};
let mut mailer = SmtpTransport::relay(&host)?.port(port).tls(tls_setting);
if let Some(creds) = creds {
mailer = mailer.credentials(creds);
}
mailer.build().send(&email)?;
Ok(())
}
pub fn send_email(
to: String,
email_title: &str,
email_description: &str,
line_two: &str,
button_info: Option<(&str, &str)>,
) -> Result<(), MailError> {
let mut email = if button_info.is_some() {
include_str!("button_notif.html")
} else {
include_str!("auth_notif.html")
}
.replace("{{ email_title }}", email_title)
.replace("{{ email_description }}", email_description)
.replace("{{ line_one }}", email_description)
.replace("{{ line_two }}", line_two);
if let Some((button_title, button_link)) = button_info {
email = email
.replace("{{ button_title }}", button_title)
.replace("{{ button_link }}", button_link);
}
send_email_raw(to, email_title.to_string(), email)?;
Ok(())
}

View File

@@ -1,5 +1,4 @@
pub mod checks;
pub mod email;
pub mod oauth;
pub mod templates;
pub mod validate;
@@ -8,9 +7,7 @@ pub use checks::{
filter_visible_collections, filter_visible_project_ids,
filter_visible_projects,
};
pub use email::send_email;
use serde::{Deserialize, Serialize};
// pub use pat::{generate_pat, PersonalAccessToken};
pub use validate::{check_is_moderator_from_headers, get_user_from_headers};
use crate::file_hosting::FileHostingError;
@@ -36,7 +33,7 @@ pub enum AuthenticationError {
#[error("Error while decoding PAT: {0}")]
Decoding(#[from] ariadne::ids::DecodingError),
#[error("{0}")]
Mail(#[from] email::MailError),
Mail(#[from] crate::queue::email::MailError),
#[error("Invalid Authentication Credentials")]
InvalidCredentials,
#[error("Authentication method was not valid")]