diff --git a/.env b/.env index 735724ee..475df852 100644 --- a/.env +++ b/.env @@ -41,3 +41,10 @@ # ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app # ROCKET_PORT=8000 # ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"} + +## Mail specific settings, if SMTP_HOST is specified, SMTP_USERNAME and SMTP_PASSWORD are mandatory +# SMTP_HOST=smtp.domain.tld +# SMTP_PORT=587 +# SMTP_SSL=true +# SMTP_USERNAME=username +# SMTP_PASSWORD=password \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 13d13c5a..99a7542c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,11 @@ lazy_static = "1.1.0" num-traits = "0.2.5" num-derive = "0.2.2" +lettre = "0.8.2" +lettre_email = "0.8.2" +native-tls = "0.1.5" +fast_chemail = "0.9.5" + [patch.crates-io] # Make jwt use ring 0.11, to match rocket jsonwebtoken = { path = "libs/jsonwebtoken" } diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index a551a868..8cf84ce8 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -5,6 +5,8 @@ use db::models::*; use api::{PasswordData, JsonResult, EmptyResult, JsonUpcase, NumberOrString}; use auth::Headers; +use fast_chemail::is_valid_email; +use mail; use CONFIG; @@ -85,7 +87,10 @@ fn post_profile(data: JsonUpcase, headers: Headers, conn: DbConn) - let mut user = headers.user; user.name = data.Name; - user.password_hint = data.MasterPasswordHint; + user.password_hint = match data.MasterPasswordHint { + Some(ref h) if h.is_empty() => None, + _ => data.MasterPasswordHint, + }; user.save(&conn); Ok(Json(user.to_json(&conn))) @@ -263,17 +268,29 @@ struct PasswordHintData { fn password_hint(data: JsonUpcase, conn: DbConn) -> EmptyResult { let data: PasswordHintData = data.into_inner().data; - if !CONFIG.show_password_hint { - return Ok(()) + if !is_valid_email(&data.Email) { + return err!("This email address is not valid..."); } - match User::find_by_mail(&data.Email, &conn) { - Some(user) => { - let hint = user.password_hint.to_owned().unwrap_or_default(); - err!(format!("Your password hint is: {}", hint)) - }, - None => Ok(()), + let user = User::find_by_mail(&data.Email, &conn); + if user.is_none() { + return Ok(()); } + + let user = user.unwrap(); + if let Some(ref mail_config) = CONFIG.mail { + if let Err(e) = mail::send_password_hint(&user.email, user.password_hint, mail_config) { + err!(format!("There have been a problem sending the email: {}", e)); + } + } else if CONFIG.show_password_hint { + if let Some(hint) = user.password_hint { + err!(format!("Your password hint is: {}", &hint)); + } else { + err!(format!("Sorry, you have no password hint...")); + } + } + + Ok(()) } #[derive(Deserialize)] diff --git a/src/mail.rs b/src/mail.rs new file mode 100644 index 00000000..ccf83cca --- /dev/null +++ b/src/mail.rs @@ -0,0 +1,63 @@ +use std::error::Error; +use native_tls::{Protocol, TlsConnector}; +use lettre::{EmailTransport, SmtpTransport, ClientTlsParameters, ClientSecurity}; +use lettre::smtp::{ConnectionReuseParameters, SmtpTransportBuilder}; +use lettre::smtp::authentication::Credentials; +use lettre_email::EmailBuilder; + +use MailConfig; + +fn mailer(config: &MailConfig) -> SmtpTransport { + let client_security = if config.smtp_ssl { + let mut tls_builder = TlsConnector::builder().unwrap(); + tls_builder.supported_protocols(&[Protocol::Tlsv11, Protocol::Tlsv12]).unwrap(); + ClientSecurity::Required( + ClientTlsParameters::new(config.smtp_host.to_owned(), tls_builder.build().unwrap()) + ) + } else { + ClientSecurity::None + }; + + let smtp_transport = SmtpTransportBuilder::new( + (config.smtp_host.to_owned().as_str(), config.smtp_port), + client_security + ).unwrap(); + + let smtp_transport = match (&config.smtp_username, &config.smtp_password) { + (Some(username), Some(password)) => { + smtp_transport.credentials(Credentials::new(username.to_owned(), password.to_owned())) + }, + (_, _) => smtp_transport, + }; + + smtp_transport + .smtp_utf8(true) + .connection_reuse(ConnectionReuseParameters::NoReuse) + .build() +} + +pub fn send_password_hint(address: &str, hint: Option, config: &MailConfig) -> Result<(), String> { + let (subject, body) = if let Some(hint) = hint { + ("Your master password hint", + format!( + "You (or someone) recently requested your master password hint.\n\n\ + Your hint is: \"{}\"\n\n\ + If you did not request your master password hint you can safely ignore this email.\n", + hint)) + } else { + ("Sorry, you have no password hint...", + "Sorry, you have not specified any password hint...\n".to_string()) + }; + + let email = EmailBuilder::new() + .to(address) + .from((config.smtp_from.to_owned(), "Bitwarden-rs")) + .subject(subject) + .body(body) + .build().unwrap(); + + match mailer(config).send(&email) { + Ok(_) => Ok(()), + Err(e) => Err(e.description().to_string()), + } +} diff --git a/src/main.rs b/src/main.rs index e715031c..9436e39b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,10 @@ extern crate lazy_static; #[macro_use] extern crate num_derive; extern crate num_traits; +extern crate lettre; +extern crate lettre_email; +extern crate native_tls; +extern crate fast_chemail; use std::{env, path::Path, process::{exit, Command}}; use rocket::Rocket; @@ -38,6 +42,7 @@ mod api; mod db; mod crypto; mod auth; +mod mail; fn init_rocket() -> Rocket { rocket::ignite() @@ -155,6 +160,57 @@ lazy_static! { static ref CONFIG: Config = Config::load(); } +#[derive(Debug)] +pub struct MailConfig { + smtp_host: String, + smtp_port: u16, + smtp_ssl: bool, + smtp_from: String, + smtp_username: Option, + smtp_password: Option, +} + +impl MailConfig { + fn load() -> Option { + let smtp_host = env::var("SMTP_HOST").ok(); + + // When SMTP_HOST is absent, we assume the user does not want to enable it. + if smtp_host.is_none() { + return None + } + + let smtp_ssl = util::parse_option_string(env::var("SMTP_SSL").ok()).unwrap_or(true); + let smtp_port = util::parse_option_string(env::var("SMTP_PORT").ok()) + .unwrap_or_else(|| { + if smtp_ssl { + 587u16 + } else { + 25u16 + } + }); + + let smtp_username = env::var("SMTP_USERNAME").ok(); + let smtp_password = env::var("SMTP_PASSWORD").ok().or_else(|| { + if smtp_username.as_ref().is_some() { + println!("Please specify SMTP_PASSWORD to enable SMTP support."); + exit(1); + } else { + None + } + }); + + Some(MailConfig { + smtp_host: smtp_host.unwrap(), + smtp_port: smtp_port, + smtp_ssl: smtp_ssl, + smtp_from: util::parse_option_string(env::var("SMTP_FROM").ok()) + .unwrap_or("bitwarden-rs@localhost".to_string()), + smtp_username: smtp_username, + smtp_password: smtp_password, + }) + } +} + #[derive(Debug)] pub struct Config { database_url: String, @@ -172,8 +228,11 @@ pub struct Config { signups_allowed: bool, password_iterations: i32, show_password_hint: bool, + domain: String, domain_set: bool, + + mail: Option, } impl Config { @@ -204,6 +263,8 @@ impl Config { domain_set: domain.is_ok(), domain: domain.unwrap_or("http://localhost".into()), + + mail: MailConfig::load(), } } }