mnml/server/src/mail.rs

302 lines
8.2 KiB
Rust

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<Response, MailError> {
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<Email, Error> {
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<Option<Email>, 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<String, Error> {
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<Email, Error> {
// 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<Mail>) -> 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;
}