From 2b96d498a736f6a82743c5750af8445174d9346b Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 30 Jul 2023 00:36:30 +0100 Subject: [PATCH] feat: new binary to take the csv and mail users about it --- src/bin/new_users.rs | 245 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 src/bin/new_users.rs diff --git a/src/bin/new_users.rs b/src/bin/new_users.rs new file mode 100644 index 0000000..c490c3d --- /dev/null +++ b/src/bin/new_users.rs @@ -0,0 +1,245 @@ +use chrono::{DateTime, SecondsFormat, Utc}; +use skynet_ldap_backend::{get_config, Config, db_init, Accounts, AccountsNew}; +use std::error::Error; +use sqlx::{Pool, Sqlite}; +use rand::{thread_rng, Rng}; +use rand::distributions::Alphanumeric; +use lettre::{ + message::{header, MultiPart, SinglePart}, + SmtpTransport, Message, Transport, + transport::smtp::{ + authentication::Credentials, + response::Response + }, +}; +use maud::html; + +#[async_std::main] +async fn main() { + let config = get_config(); + let db = db_init(&config).await.unwrap(); + let now = Utc::now(); + + if let Ok(records) = read_csv(&config){ + for record in records { + // check if the email is already in the db + if !check(&db, &record.email).await { + continue; + } + + // generate a auth key + let auth = generate_auth(); + + match send_mail(&config, &record, &auth) { + Ok(_) => { + match save_to_db(&db, now, &record, &auth).await { + Ok(_) => {} + Err(e) => { + println!("Unable to save to db {} {e:?}", &record.email); + } + } + } + Err(e) => { + println!("Unable to send mail to {} {e:?}", &record.email); + } + } + } + } +} + + +#[derive(Debug, serde::Deserialize)] +struct Record { + #[serde(rename = "MemID")] + mem_id: String, + #[serde(rename = "Student Num")] + id_student: String, + #[serde(rename = "Contact Email")] + email: String, + #[serde(rename = "Expiry")] + expiry: String, + #[serde(rename = "First Name")] + name_first: String, + #[serde(rename = "Last Name")] + name_second: String, +} + +fn read_csv(config: &Config) -> Result, Box> { + let mut records: Vec = vec![]; + + if let Ok(mut rdr) = csv::Reader::from_path(format!("{}/{}", &config.home, &config.csv)) { + for result in rdr.deserialize() { + // Notice that we need to provide a type hint for automatic + // deserialization. + let record: Record = result?; + if record.mem_id == "" { + continue; + } + records.push(record); + } + } + + Ok(records) +} + +async fn check(db: &Pool, mail: &str) -> bool { + check_pending(db, mail).await && check_users(db, mail).await +} +async fn check_users(db: &Pool, mail: &str) -> bool { + sqlx::query_as::<_, Accounts>( + r#" + SELECT * + FROM accounts + WHERE mail == ? + "#, + ) + .bind(mail) + .fetch_all(db) + .await + .unwrap_or(vec![]) + .is_empty() +} +async fn check_pending(db: &Pool, mail: &str) -> bool { + sqlx::query_as::<_, Accounts>( + r#" + SELECT * + FROM accounts_new + WHERE mail == ? + "#, + ) + .bind(mail) + .fetch_all(db) + .await + .unwrap_or(vec![]) + .is_empty() +} + +// from https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html#create-random-passwords-from-a-set-of-alphanumeric-characters +fn generate_auth() -> String { + thread_rng() + .sample_iter(&Alphanumeric) + .take(30) + .map(char::from) + .collect() +} + +// using https://github.com/lettre/lettre/blob/57886c367d69b4d66300b322c94bd910b1eca364/examples/maud_html.rs +fn send_mail(config: &Config, record: &Record, auth: &str) -> Result { + let recipient = &record.name_first; + let mail = &record.email; + let url_base = "https://sso.skynet.ie"; + let link_new = format!("{url_base}/register?auth={auth}"); + let link_mod = format!("{url_base}/modify"); + let discord = "https://discord.gg/mkuKJkCuyM"; + let sender = format!("UL Computer Society <{}>", &config.mail_user); + + // Create the html we want to send. + let html = html! { + head { + title { "Hello from Skynet!" } + style type="text/css" { + "h2, h4 { font-family: Arial, Helvetica, sans-serif; }" + } + } + div style="display: flex; flex-direction: column; align-items: center;" { + h2 { "Hello from Skynet!" } + // Substitute in the name of our recipient. + p { "Hi " (recipient) "," } + p { + "If you are a new member please use the following link:" + br; + a href=(link_new) { (link_new) } + } + p { + "If you are a returning user please set an email for your account at:" + br; + a href=(link_mod) { (link_mod) } + } + p { + "If you have issues please refer to our Discord server:" + br; + a href=(discord) { (discord) } + } + + p { + "UL Computer Society" + br; + "Skynet Team" + } + } + }; + + let body_text = format!( + r#" + Hi {recipient} + + If you are a new member please use the following link: + {link_new} + + If you are a returning user please set an email for your account at: + {link_mod} + + If you have issues please refer to our Discord server: + {discord} + + UL Computer Society + Skynet Team + "# + ); + + // Build the message. + let email = Message::builder() + .from(sender.parse().unwrap()) + .to(mail.parse().unwrap()) + .subject("Skynet: New Account.") + .multipart( + // This is composed of two parts. + // also helps not trip spam settings (uneven number of url's + MultiPart::alternative() + .singlepart( + SinglePart::builder() + .header(header::ContentType::TEXT_PLAIN) + .body(body_text), + ) + .singlepart( + SinglePart::builder() + .header(header::ContentType::TEXT_HTML) + .body(html.into_string()), + ), + ) + .expect("failed to build email"); + + + let creds = Credentials::new(config.mail_user.clone(), config.mail_pass.clone()); + + // Open a remote connection to gmail using STARTTLS + let mailer = SmtpTransport::starttls_relay(&config.mail_smtp) + .unwrap() + .credentials(creds) + .build(); + + // Send the email + mailer.send(&email) +} + +async fn save_to_db(db: &Pool, now: DateTime, record: &Record, auth: &str) -> Result, sqlx::Error> { + sqlx::query_as::<_, AccountsNew>( + " + INSERT OR REPLACE INTO accounts_new (mail, auth_code, date_iso, date_expiry, name_first, name_surname) + VALUES (?1, ?2, ?3, ?4, ?5, ?6) + ", + ) + .bind(record.email.to_owned()) + .bind(auth.to_owned()) + .bind(now.to_rfc3339_opts(SecondsFormat::Millis, true)) + .bind(record.expiry.to_owned()) + .bind(record.name_first.to_owned()) + .bind(record.name_second.to_owned()) + .fetch_optional(db) + .await +} + + + + +