use lettre::{ message::{header, MultiPart, SinglePart}, transport::smtp::{self, authentication::Credentials}, Message, SmtpTransport, Transport, }; use maud::html; use serenity::{ builder::CreateApplicationCommand, client::Context, model::{ application::interaction::application_command::ApplicationCommandInteraction, prelude::{command::CommandOptionType, interaction::application_command::CommandDataOptionValue}, }, }; use skynet_discord_bot::{get_now_iso, random_string, Config, DataBase, Wolves, WolvesVerify}; use sqlx::{Pool, Sqlite}; pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String { let db_lock = { let data_read = ctx.data.read().await; data_read.get::().expect("Expected Databse in TypeMap.").clone() }; let db = db_lock.read().await; let config_lock = { let data_read = ctx.data.read().await; data_read.get::().expect("Expected Config in TypeMap.").clone() }; let config = config_lock.read().await; if get_server_member_discord(&db, &command.user.name).await.is_some() { return "Already linked".to_string(); } let option = command .data .options .get(0) .expect("Expected email option") .resolved .as_ref() .expect("Expected email object"); let email = if let CommandDataOptionValue::String(email) = option { email } else { return "Please provide a valid user".to_string(); }; // check if email exists let details = match get_server_member_email(&db, email).await { None => return "Please check is the same as on https://ulwolves.ie/".to_string(), Some(x) => x, }; if details.verified { return "Email already verified".to_string(); } db_pending_clear_expired(&db).await; // send mail if get_from_db(&db, &command.user.name).await.is_some() { return "Please check email".to_string(); } // generate a auth key let auth = random_string(20); match send_mail(&config, &details, &auth, &command.user.name) { Ok(_) => match save_to_db(&db, &details, &auth, &command.user.name).await { Ok(_) => {} Err(e) => { return format!("Unable to save to db {} {e:?}", &details.email); } }, Err(e) => { return format!("Unable to send mail to {} {e:?}", &details.email); } } format!("Verification email sent to {}, user is {} {:?}", email, command.user.name, command.guild_id) } pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { command .name("link") .description("Set Wolves Email") .create_option(|option| option.name("email").description("UL Wolves Email").kind(CommandOptionType::String).required(true)) } async fn get_server_member_discord(db: &Pool, user: &str) -> Option { sqlx::query_as::<_, Wolves>( r#" SELECT * FROM wolves WHERE discord = "#, ) .bind(user) .fetch_one(db) .await .ok() } async fn get_server_member_email(db: &Pool, email: &str) -> Option { sqlx::query_as::<_, Wolves>( r#" SELECT * FROM wolves WHERE email = ? "#, ) .bind(email) .fetch_one(db) .await .ok() } fn send_mail(config: &Config, email: &Wolves, auth: &str, user: &str) -> Result { let mail = &email.email; let discord = "https://discord.skynet.ie"; 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 " (user) "," } p { "Please use " pre { "/verify " (auth)} " to verify your discord account." } p { "If you have issues please refer to our Discord server:" br; a href=(discord) { (discord) } } p { "Skynet Team" br; "UL Computer Society" } } }; let body_text = format!( r#" Hi {user} Please use "/verify {auth}" to verify your discord account. If you have issues please refer to our Discord server: {discord} Skynet Team UL Computer Society "# ); // Build the message. let email = Message::builder() .from(sender.parse().unwrap()) .to(mail.parse().unwrap()) .subject("Skynet-Discord: Link Wolves.") .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 db_pending_clear_expired(pool: &Pool) -> Option { sqlx::query_as::<_, WolvesVerify>( r#" DELETE FROM wolves_verify WHERE date_expiry < ? "#, ) .bind(get_now_iso(true)) .fetch_one(pool) .await .ok() } async fn get_from_db(db: &Pool, user: &str) -> Option { sqlx::query_as::<_, WolvesVerify>( r#" SELECT * FROM wolves_verify WHERE discord = "#, ) .bind(user) .fetch_one(db) .await .ok() } async fn save_to_db(db: &Pool, record: &Wolves, auth: &str, user: &str) -> Result, sqlx::Error> { sqlx::query_as::<_, WolvesVerify>( " INSERT INTO wolves_verify (email, discord, auth_code, date_expiry) VALUES (?1, ?2, ?3, ?4) ", ) .bind(record.email.to_owned()) .bind(user) .bind(auth.to_owned()) .bind(get_now_iso(false)) .fetch_optional(db) .await }