diff --git a/.env b/.env index 735724e..475df85 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 fb5adaf..537b4e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,10 @@ lazy_static = "1.0.1" num-traits = "0.2.5" num-derive = "0.2.2" +lettre = "0.8.2" +lettre_email = "0.8.2" +native-tls = "0.1.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 e45c08d..bc018f1 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -5,6 +5,7 @@ use db::models::*; use api::{PasswordData, JsonResult, EmptyResult, JsonUpcase, NumberOrString}; use auth::Headers; +use mail; use CONFIG; @@ -258,15 +259,23 @@ struct PasswordHintData { fn password_hint(data: JsonUpcase, conn: DbConn) -> EmptyResult { let data: PasswordHintData = data.into_inner().data; - if !CONFIG.show_password_hint { - return Ok(()) + let user = User::find_by_mail(&data.Email, &conn); + if user.is_none() { + return Ok(()); } - 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.unwrap(); + let hint = user.password_hint.to_owned().unwrap_or("You don't have any...".to_string()); + + if let Some(ref mail_config) = CONFIG.mail { + if let Err(e) = mail::send_password_hint(&user.email, &hint, mail_config) { + err!(format!("There have been a problem sending the email: {}", e)); + } } + + if !CONFIG.show_password_hint { + err!(format!("Your password hint is: {}", &hint)); + } + + Ok(()) } diff --git a/src/mail.rs b/src/mail.rs new file mode 100644 index 0000000..7faa791 --- /dev/null +++ b/src/mail.rs @@ -0,0 +1,47 @@ +use std::error::Error; +use native_tls::TlsConnector; +use native_tls::{Protocol}; +use lettre::{EmailTransport, SmtpTransport, ClientTlsParameters, ClientSecurity}; +use lettre::smtp::{ConnectionReuseParameters, SmtpTransportBuilder}; +use lettre::smtp::authentication::{Credentials, Mechanism}; +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::Tlsv10, Protocol::Tlsv11, Protocol::Tlsv12 + ]).unwrap(); + + ClientSecurity::Required( + ClientTlsParameters::new(config.smtp_host.to_owned(), tls_builder.build().unwrap()) + ) + } else { + ClientSecurity::None + }; + + SmtpTransportBuilder::new((config.smtp_host.to_owned().as_str(), config.smtp_port), client_security) + .unwrap() + .credentials(Credentials::new(config.smtp_username.to_owned(), config.smtp_password.to_owned())) + .authentication_mechanism(Mechanism::Login) + .smtp_utf8(true) + .connection_reuse(ConnectionReuseParameters::ReuseUnlimited) + .build() +} + +pub fn send_password_hint(address: &str, hint: &str, config: &MailConfig) -> Result<(), String> { + let email = EmailBuilder::new() + .to(address) + .from((config.smtp_from.to_owned(), "Bitwarden-rs")) + .subject("Your Master Password Hint") + .body(hint) + .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 e506e05..8f39057 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,9 @@ 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; use std::{env, path::Path, process::{exit, Command}}; use rocket::Rocket; @@ -37,6 +40,7 @@ mod api; mod db; mod crypto; mod auth; +mod mail; fn init_rocket() -> Rocket { rocket::ignite() @@ -155,10 +159,10 @@ lazy_static! { #[derive(Debug)] pub struct MailConfig { - reply_to_email: Option, smtp_host: String, smtp_port: u16, smtp_ssl: bool, + smtp_from: String, smtp_username: String, smtp_password: String, } @@ -172,22 +176,23 @@ impl MailConfig { return None } - let smtp_ssl = util::parse_option_string(env::var("SMTP_SSL").ok()).unwrap_or(false); + 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 { - 465u16 + 587u16 } else { 25u16 } }); Some(MailConfig { - reply_to_email: util::parse_option_string(env::var("REPLY_TO_EMAIL").ok()), smtp_host: smtp_host.unwrap(), smtp_port: smtp_port, smtp_ssl: smtp_ssl, - // If username or password is not specified, and SMTP support seems to be wanted, + smtp_from: util::parse_option_string(env::var("SMTP_FROM").ok()) + .unwrap_or("bitwarden@localhost".to_string()), + // If username or password is not specified and SMTP support seems to be wanted, // don't let the app start: the configuration is clearly incomplete. smtp_username: util::parse_option_string(env::var("SMTP_USERNAME").ok()) .unwrap_or_else(|| {