use std::env; use uuid::Uuid; use rand::{thread_rng, Rng}; use rand::distributions::Alphanumeric; use std::iter; use postgres::transaction::Transaction; use failure::Error; use failure::{err_msg, format_err}; use crossbeam_channel::Receiver; use lettre::smtp::authentication::{Credentials, Mechanism}; use lettre::smtp::ConnectionReuseParameters; use lettre::smtp::error::Error as MailError; use lettre::smtp::extension::ClientId; use lettre::smtp::response::Response; use lettre::{SendableEmail, SmtpClient, SmtpTransport, Transport}; use lettre_email::Email as LettreEmail; use account::Account; use pg::Db; #[derive(Debug,Clone,Serialize)] pub struct Email { pub id: Uuid, pub email: String, pub account: Uuid, pub confirmed: bool, } #[derive(Debug)] pub enum Mail { Recover { email: String, name: String, token: String }, Confirm { email: String, name: String, token: String }, } // create link that will set a token // put msg saying pls reset your password // redirect to main page cause cbf fn recover(email: &String, name: &String, token: &String) -> SendableEmail { let body = format!("{:}, the link below will recover your account. please change your password immediately in the account page. this link will expire in 48 hours or once used. http://mnml.gg/api/account/recover?recover_token={:} glhf --mnml", name, token); LettreEmail::builder() .from("machines@mnml.gg") .to(email.clone()) .subject("account recovery") .text(body) .build() .unwrap() .into() } fn confirm(email: &String, name: &String, token: &String) -> SendableEmail { let confirm_body = format!("{:}, please click the link below to confirm your email http://mnml.gg/api/account/email/confirm?confirm_token={:} glhf --mnml", name, token); LettreEmail::builder() .from("machines@mnml.gg") .to(email.clone()) .subject("email confirmation") .text(confirm_body) .build() .unwrap() .into() } pub fn send_mail(mailer: &mut SmtpTransport, mail: Mail) -> Result { let msg = match mail { Mail::Recover { email, name, token } => recover(&email, &name, &token), Mail::Confirm { email, name, token } => confirm(&email, &name, &token), }; mailer.send(msg) } pub fn confirm_email(tx: &mut Transaction, account: &Account, confirm_token: String) -> Result<(String, Uuid), Error> { let query = " UPDATE emails SET confirmed = true, updated_at = now() WHERE confirm_token = $1 AND account = $2 RETURNING id, email, account "; let result = tx .query(query, &[&confirm_token, &account.id])?; let row = result.iter().next() .ok_or(format_err!("confirm_token not found {:?}", confirm_token))?; let _id: Uuid = row.get(0); let email: String = row.get(1); let account: Uuid = row.get(2); return Ok((email, account)); } pub fn select(db: &Db, email: &String) -> Result { let query = " SELECT id, email, account, confirmed FROM emails WHERE email = $1; "; let result = db .query(query, &[&email])?; let row = result.iter().next() .ok_or(err_msg("email found"))?; let id: Uuid = row.get(0); let email: String = row.get(1); let account: Uuid = row.get(2); let confirmed: bool = row.get(3); return Ok(Email { id, email, account, confirmed }); } pub fn select_account(db: &Db, account: Uuid) -> Result, Error> { let query = " SELECT id, email, account, confirmed FROM emails WHERE account = $1; "; let result = db .query(query, &[&account])?; let row = match result.iter().next() { Some(r) => r, None => return Ok(None), }; let id: Uuid = row.get(0); let email: String = row.get(1); let account: Uuid = row.get(2); let confirmed: bool = row.get(3); return Ok(Some(Email { id, email, account, confirmed })); } pub fn set_recovery(tx: &mut Transaction, email: &String) -> Result { let mut rng = thread_rng(); let recover_token: String = iter::repeat(()) .map(|()| rng.sample(Alphanumeric)) .take(64) .collect(); let query = " UPDATE emails SET recover_token = $1, recover_token_expiry = now() + interval '2 days' WHERE email = $2 AND confirmed = true RETURNING id, email, account "; let result = tx .query(query, &[&recover_token, &email])?; let row = result.iter().next() .ok_or(format_err!("no confirmed email found {:?}", email))?; let _id: Uuid = row.get(0); let _email: String = row.get(1); let _account: Uuid = row.get(2); return Ok(recover_token); } pub fn get_recovery(tx: &mut Transaction, recover_token: &String) -> Result { // set a new token when recovering to prevent multiple access let mut rng = thread_rng(); let new_token: String = iter::repeat(()) .map(|()| rng.sample(Alphanumeric)) .take(64) .collect(); let query = " UPDATE emails SET recover_token = $1, recover_token_expiry = now() WHERE recover_token = $2 AND recover_token_expiry > now() AND confirmed = true RETURNING id, email, account, confirmed; "; let result = tx .query(query, &[&new_token, &recover_token])?; let row = result.iter().next() .ok_or(err_msg("no confirmed email found"))?; let id: Uuid = row.get(0); let email: String = row.get(1); let account: Uuid = row.get(2); let confirmed: bool = row.get(3); return Ok(Email { id, email, account, confirmed }); } pub fn set(tx: &mut Transaction, account: Uuid, email: &String) -> Result<(Uuid, String), Error> { let id = Uuid::new_v4(); let mut rng = thread_rng(); let confirm_token: String = iter::repeat(()) .map(|()| rng.sample(Alphanumeric)) .take(64) .collect(); let recover_token: String = iter::repeat(()) .map(|()| rng.sample(Alphanumeric)) .take(64) .collect(); let insert_query = " INSERT INTO emails (id, account, email, confirm_token, confirmed, recover_token) VALUES ($1, $2, $3, $4, false, $5) RETURNING id; "; let update_query = " UPDATE emails SET email = $1, confirm_token = $2, confirmed = false, recover_token = $3 WHERE account = $4 RETURNING id; "; let result = match tx.query(insert_query, &[&id, &account, &email, &confirm_token, &recover_token]) { Ok(r) => r, // email update probably Err(_) => { match tx.query(update_query, &[&email, &confirm_token, &recover_token, &account]) { Ok(r) => r, Err(e) => { warn!("{:?}", e); return Err(err_msg("no email set")); }, } } }; match result.iter().next() { Some(row) => row, None => return Err(err_msg("no email set")), }; return Ok((id, confirm_token)); } pub fn listen(rx: Receiver) -> SmtpTransport { let sender = env::var("MAIL_ADDRESS") .expect("MAIL_ADDRESS must be set"); let password = env::var("MAIL_PASSWORD") .expect("MAIL_PASSWORD must be set"); let domain = env::var("MAIL_DOMAIN") .expect("MAIL_DOMAIN must be set"); let mut mailer = SmtpClient::new_simple("smtp.gmail.com").unwrap() .hello_name(ClientId::Domain(domain)) .credentials(Credentials::new(sender, password)) .smtp_utf8(true) .authentication_mechanism(Mechanism::Plain) .connection_reuse(ConnectionReuseParameters::ReuseUnlimited) .transport(); info!("mail connected"); // loop { // match rx.recv() { // Ok(m) => match send_mail(&mut mailer, m) { // Ok(r) => info!("{:?}", r), // Err(e) => warn!("{:?}", e), // }, // Err(e) => { // error!("{:?}", e); // panic!("mail thread cannot continue"); // }, // }; // } // Explicitly close the SMTP transaction as we enabled connection reuse // mailer.close(); return mailer; }