Notify users by email when locking their account (wip: error handling)
This commit is contained in:
parent
d4af61fb35
commit
a3c9105caa
4 changed files with 225 additions and 9 deletions
126
Cargo.lock
generated
126
Cargo.lock
generated
|
@ -260,6 +260,12 @@ dependencies = [
|
||||||
"alloc-no-stdlib",
|
"alloc-no-stdlib",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allocator-api2"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android-tzdata"
|
name = "android-tzdata"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
@ -456,6 +462,16 @@ dependencies = [
|
||||||
"phf_codegen",
|
"phf_codegen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chumsky"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
"stacker",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
@ -592,6 +608,22 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "email-encoding"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ea3d894bbbab314476b265f9b2d46bf24b123a36dd0e96b06a1b49545b9d9dcc"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "email_address"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.35"
|
version = "0.8.35"
|
||||||
|
@ -663,6 +695,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"forgejo-api",
|
"forgejo-api",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"lettre",
|
||||||
"rand",
|
"rand",
|
||||||
"reqwest 0.12.9",
|
"reqwest 0.12.9",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -829,6 +862,16 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"allocator-api2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.1"
|
version = "0.15.1"
|
||||||
|
@ -1224,7 +1267,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
|
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.15.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1269,6 +1312,32 @@ version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lettre"
|
||||||
|
version = "0.11.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab4c9a167ff73df98a5ecc07e8bf5ce90b583665da3d1762eb1f775ad4d0d6f5"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"chumsky",
|
||||||
|
"email-encoding",
|
||||||
|
"email_address",
|
||||||
|
"fastrand",
|
||||||
|
"httpdate",
|
||||||
|
"idna",
|
||||||
|
"mime",
|
||||||
|
"nom",
|
||||||
|
"percent-encoding",
|
||||||
|
"quoted_printable",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pemfile 2.2.0",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"socket2",
|
||||||
|
"tokio",
|
||||||
|
"url",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.164"
|
version = "0.2.164"
|
||||||
|
@ -1348,6 +1417,12 @@ dependencies = [
|
||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minimal-lexical"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
@ -1387,6 +1462,16 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "7.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"minimal-lexical",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -1630,6 +1715,15 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psm"
|
||||||
|
version = "0.1.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.37"
|
version = "1.0.37"
|
||||||
|
@ -1639,6 +1733,12 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quoted_printable"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
|
@ -1846,7 +1946,9 @@ version = "0.23.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f1a745511c54ba6d4465e8d5dfbd81b45791756de28d4981af70d6dca128f1e"
|
checksum = "7f1a745511c54ba6d4465e8d5dfbd81b45791756de28d4981af70d6dca128f1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-webpki",
|
"rustls-webpki",
|
||||||
"subtle",
|
"subtle",
|
||||||
|
@ -2087,6 +2189,19 @@ version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stacker"
|
||||||
|
version = "0.1.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"psm",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
|
@ -2593,6 +2708,15 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "0.26.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
|
|
|
@ -19,6 +19,7 @@ tera = "1"
|
||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
actix-files = "0.6"
|
actix-files = "0.6"
|
||||||
unicode-segmentation = "1"
|
unicode-segmentation = "1"
|
||||||
|
lettre = { version = "0.11", features = ["builder", "smtp-transport", "rustls-tls"], default-features = false }
|
||||||
|
|
||||||
[profile.profiling]
|
[profile.profiling]
|
||||||
inherits = "dev"
|
inherits = "dev"
|
||||||
|
|
79
src/email.rs
Normal file
79
src/email.rs
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use lettre::{Message, SmtpTransport, Transport};
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
pub struct SmtpConfig {
|
||||||
|
address: String,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmtpConfig {
|
||||||
|
pub fn mailer(&self) -> Result<SmtpTransport, lettre::transport::smtp::Error> {
|
||||||
|
use lettre::transport::smtp::authentication::Credentials;
|
||||||
|
Ok(SmtpTransport::relay(&self.address)?
|
||||||
|
.credentials(Credentials::new(
|
||||||
|
self.username.to_owned(),
|
||||||
|
self.password.to_owned(),
|
||||||
|
))
|
||||||
|
.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn from_env() -> anyhow::Result<SmtpConfig> {
|
||||||
|
let address = env::var("SMTP_ADDRESS")?;
|
||||||
|
let username = env::var("SMTP_USERNAME")?;
|
||||||
|
let password = env::var("SMTP_PASSWORD")?;
|
||||||
|
let smtp = SmtpConfig {
|
||||||
|
address,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
};
|
||||||
|
if !smtp.mailer()?.test_connection()? {
|
||||||
|
return Err(anyhow!("Unable to contact the SMTP relay"));
|
||||||
|
}
|
||||||
|
Ok(smtp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_locked_account_notice(
|
||||||
|
smtp: &SmtpConfig,
|
||||||
|
login: &str,
|
||||||
|
email: &str,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let grace_period_days = crate::GRACE_PERIOD.as_secs() / (24 * 3600);
|
||||||
|
|
||||||
|
let email = Message::builder()
|
||||||
|
.from(smtp.username.parse().unwrap())
|
||||||
|
.to(email.parse()?)
|
||||||
|
.subject(format!(
|
||||||
|
"[Forgejo Deuxfleurs] Your account was marked as spam and will be deleted in {} days",
|
||||||
|
grace_period_days
|
||||||
|
))
|
||||||
|
.body(format!(
|
||||||
|
"\
|
||||||
|
(English version below)
|
||||||
|
|
||||||
|
Ceci est un email pour vous informer que votre compte utilisateur \"{login}\" sur \
|
||||||
|
https://git.deuxfleurs.fr a été considéré comme spam par un administrateur.
|
||||||
|
|
||||||
|
Par défaut, ce compte sera par conséquent supprimé dans {grace_period_days} jours.
|
||||||
|
|
||||||
|
S'il s'agit d'une erreur de notre part, merci de nous contacter rapidement par email via \
|
||||||
|
<prod-sysadmin@deuxfleurs.fr>.
|
||||||
|
|
||||||
|
-- les administrateurs de Deuxfleurs
|
||||||
|
|
||||||
|
--------
|
||||||
|
|
||||||
|
This is an email to inform you that your user account \"{login}\" at https://git.deuxfleurs.fr \
|
||||||
|
was considered as spam by an administrator.
|
||||||
|
|
||||||
|
Without further action on your part, the account will be deleted in {grace_period_days} days.
|
||||||
|
|
||||||
|
If you believe this is a mistake, please reach out quickly by email at <prod-sysadmin@deuxfleurs.fr>.
|
||||||
|
|
||||||
|
-- the Deuxfleurs admins"))
|
||||||
|
.unwrap();
|
||||||
|
smtp.mailer()?.send(&email)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
24
src/main.rs
24
src/main.rs
|
@ -13,12 +13,14 @@ use tera::Tera;
|
||||||
mod classifier;
|
mod classifier;
|
||||||
mod data;
|
mod data;
|
||||||
mod db;
|
mod db;
|
||||||
|
mod email;
|
||||||
mod scrape;
|
mod scrape;
|
||||||
mod workers;
|
mod workers;
|
||||||
|
|
||||||
use classifier::Classifier;
|
use classifier::Classifier;
|
||||||
use data::*;
|
use data::*;
|
||||||
use db::{Db, IsSpam};
|
use db::{Db, IsSpam};
|
||||||
|
use email::SmtpConfig;
|
||||||
|
|
||||||
// Fetch user data from forgejo from time to time
|
// Fetch user data from forgejo from time to time
|
||||||
const FORGEJO_POLL_DELAY: Duration = Duration::from_secs(11 * 3600); // 11 hours
|
const FORGEJO_POLL_DELAY: Duration = Duration::from_secs(11 * 3600); // 11 hours
|
||||||
|
@ -191,6 +193,7 @@ async fn lock_user_account(forge: &Forgejo, username: &str) -> anyhow::Result<()
|
||||||
|
|
||||||
async fn apply_classification(
|
async fn apply_classification(
|
||||||
forge: &Forgejo,
|
forge: &Forgejo,
|
||||||
|
mailer: &SmtpConfig,
|
||||||
db: &mut Db,
|
db: &mut Db,
|
||||||
classifier: &mut Classifier,
|
classifier: &mut Classifier,
|
||||||
ids: &[(UserId, bool)],
|
ids: &[(UserId, bool)],
|
||||||
|
@ -199,10 +202,10 @@ async fn apply_classification(
|
||||||
let spammers = set_spam(db, classifier, ids, overwrite);
|
let spammers = set_spam(db, classifier, ids, overwrite);
|
||||||
|
|
||||||
for user in spammers {
|
for user in spammers {
|
||||||
// TODO: send email (what do we do if sending the email didn't work?)
|
let user = &db.users.get(&user).unwrap();
|
||||||
// TODO: batch the email sending? (only open one smtp connection)
|
lock_user_account(forge, &user.login).await?;
|
||||||
lock_user_account(forge, &db.users.get(&user).unwrap().login).await?;
|
email::send_locked_account_notice(&mailer, &user.login, &user.email).await?;
|
||||||
// TODO: better error handling: retries, ..?
|
// TODO: better and more robust error handling: retries, or a worker to send the emails ..?
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -224,6 +227,7 @@ struct AppState {
|
||||||
db: Arc<Mutex<Db>>,
|
db: Arc<Mutex<Db>>,
|
||||||
classifier: Arc<Mutex<Classifier>>,
|
classifier: Arc<Mutex<Classifier>>,
|
||||||
forge: Arc<Forgejo>,
|
forge: Arc<Forgejo>,
|
||||||
|
mailer: Arc<SmtpConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
@ -323,14 +327,20 @@ async fn post_classified(
|
||||||
|
|
||||||
let db = &mut data.db.lock().unwrap();
|
let db = &mut data.db.lock().unwrap();
|
||||||
let classifier = &mut data.classifier.lock().unwrap();
|
let classifier = &mut data.classifier.lock().unwrap();
|
||||||
let forge = &data.forge;
|
|
||||||
|
|
||||||
let updates: Vec<(UserId, bool)> = form
|
let updates: Vec<(UserId, bool)> = form
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(id, classification)| (UserId(*id), classification == "spam"))
|
.map(|(id, classification)| (UserId(*id), classification == "spam"))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
apply_classification(forge, db, classifier, &updates, overwrite)
|
apply_classification(
|
||||||
|
&data.forge,
|
||||||
|
&data.mailer,
|
||||||
|
db,
|
||||||
|
classifier,
|
||||||
|
&updates,
|
||||||
|
overwrite,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap(); // FIXME
|
.unwrap(); // FIXME
|
||||||
|
|
||||||
|
@ -401,6 +411,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
|
|
||||||
eprintln!("Load users and repos");
|
eprintln!("Load users and repos");
|
||||||
let forge = Arc::new(forge().unwrap() /* FIXME */);
|
let forge = Arc::new(forge().unwrap() /* FIXME */);
|
||||||
|
let mailer = Arc::new(SmtpConfig::from_env().await.unwrap() /* FIXME */);
|
||||||
let (db, classifier) = load_db(&forge).await.unwrap(); // FIXME
|
let (db, classifier) = load_db(&forge).await.unwrap(); // FIXME
|
||||||
let db = Arc::new(Mutex::new(db));
|
let db = Arc::new(Mutex::new(db));
|
||||||
let classifier = Arc::new(Mutex::new(classifier));
|
let classifier = Arc::new(Mutex::new(classifier));
|
||||||
|
@ -409,6 +420,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
db: db.clone(),
|
db: db.clone(),
|
||||||
classifier: classifier.clone(),
|
classifier: classifier.clone(),
|
||||||
forge: forge.clone(),
|
forge: forge.clone(),
|
||||||
|
mailer: mailer.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let _ = {
|
let _ = {
|
||||||
|
|
Loading…
Reference in a new issue