use lettre::{ message::{header, MultiPart, SinglePart}, transport::smtp::{authentication::Credentials, response::Response}, Message, SmtpTransport, Transport, }; use maud::html; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use skynet_ldap_backend::{db_init, get_config, read_csv, Accounts, AccountsNew, Config, Record, get_now_iso}; use sqlx::{Pool, Sqlite}; #[async_std::main] async fn main() { let config = get_config(); let db = db_init(&config).await.unwrap(); if let Ok(records) = read_csv(&config) { for record in records { // skynet emails not permitted if record.email.trim().ends_with("@skynet.ie") { continue; } // 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, &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); } } } } } 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, 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(get_now_iso(false)) .bind(record.expiry.to_owned()) .bind(record.name_first.to_owned()) .bind(record.name_second.to_owned()) .fetch_optional(db) .await }