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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
|
@ -456,6 +462,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
|
@ -592,6 +608,22 @@ dependencies = [
|
|||
"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]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
|
@ -663,6 +695,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"forgejo-api",
|
||||
"lazy_static",
|
||||
"lettre",
|
||||
"rand",
|
||||
"reqwest 0.12.9",
|
||||
"serde",
|
||||
|
@ -829,6 +862,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.1"
|
||||
|
@ -1224,7 +1267,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1269,6 +1312,32 @@ version = "1.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "libc"
|
||||
version = "0.2.164"
|
||||
|
@ -1348,6 +1417,12 @@ dependencies = [
|
|||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.0"
|
||||
|
@ -1387,6 +1462,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
|
@ -1630,6 +1715,15 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psm"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.37"
|
||||
|
@ -1639,6 +1733,12 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quoted_printable"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
|
@ -1846,7 +1946,9 @@ version = "0.23.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f1a745511c54ba6d4465e8d5dfbd81b45791756de28d4981af70d6dca128f1e"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
|
@ -2087,6 +2189,19 @@ version = "1.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
|
@ -2593,6 +2708,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.9"
|
||||
|
|
|
@ -19,6 +19,7 @@ tera = "1"
|
|||
lazy_static = "1"
|
||||
actix-files = "0.6"
|
||||
unicode-segmentation = "1"
|
||||
lettre = { version = "0.11", features = ["builder", "smtp-transport", "rustls-tls"], default-features = false }
|
||||
|
||||
[profile.profiling]
|
||||
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 data;
|
||||
mod db;
|
||||
mod email;
|
||||
mod scrape;
|
||||
mod workers;
|
||||
|
||||
use classifier::Classifier;
|
||||
use data::*;
|
||||
use db::{Db, IsSpam};
|
||||
use email::SmtpConfig;
|
||||
|
||||
// Fetch user data from forgejo from time to time
|
||||
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(
|
||||
forge: &Forgejo,
|
||||
mailer: &SmtpConfig,
|
||||
db: &mut Db,
|
||||
classifier: &mut Classifier,
|
||||
ids: &[(UserId, bool)],
|
||||
|
@ -199,10 +202,10 @@ async fn apply_classification(
|
|||
let spammers = set_spam(db, classifier, ids, overwrite);
|
||||
|
||||
for user in spammers {
|
||||
// TODO: send email (what do we do if sending the email didn't work?)
|
||||
// TODO: batch the email sending? (only open one smtp connection)
|
||||
lock_user_account(forge, &db.users.get(&user).unwrap().login).await?;
|
||||
// TODO: better error handling: retries, ..?
|
||||
let user = &db.users.get(&user).unwrap();
|
||||
lock_user_account(forge, &user.login).await?;
|
||||
email::send_locked_account_notice(&mailer, &user.login, &user.email).await?;
|
||||
// TODO: better and more robust error handling: retries, or a worker to send the emails ..?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -224,6 +227,7 @@ struct AppState {
|
|||
db: Arc<Mutex<Db>>,
|
||||
classifier: Arc<Mutex<Classifier>>,
|
||||
forge: Arc<Forgejo>,
|
||||
mailer: Arc<SmtpConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
@ -323,14 +327,20 @@ async fn post_classified(
|
|||
|
||||
let db = &mut data.db.lock().unwrap();
|
||||
let classifier = &mut data.classifier.lock().unwrap();
|
||||
let forge = &data.forge;
|
||||
|
||||
let updates: Vec<(UserId, bool)> = form
|
||||
.iter()
|
||||
.map(|(id, classification)| (UserId(*id), classification == "spam"))
|
||||
.collect();
|
||||
|
||||
apply_classification(forge, db, classifier, &updates, overwrite)
|
||||
apply_classification(
|
||||
&data.forge,
|
||||
&data.mailer,
|
||||
db,
|
||||
classifier,
|
||||
&updates,
|
||||
overwrite,
|
||||
)
|
||||
.await
|
||||
.unwrap(); // FIXME
|
||||
|
||||
|
@ -401,6 +411,7 @@ async fn main() -> std::io::Result<()> {
|
|||
|
||||
eprintln!("Load users and repos");
|
||||
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 = Arc::new(Mutex::new(db));
|
||||
let classifier = Arc::new(Mutex::new(classifier));
|
||||
|
@ -409,6 +420,7 @@ async fn main() -> std::io::Result<()> {
|
|||
db: db.clone(),
|
||||
classifier: classifier.clone(),
|
||||
forge: forge.clone(),
|
||||
mailer: mailer.clone(),
|
||||
});
|
||||
|
||||
let _ = {
|
||||
|
|
Loading…
Reference in a new issue