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 }