Ver código fonte

Implement change-email, email-verification, account-recovery, and welcome notifications

tomuta 5 anos atrás
pai
commit
bd1e8be328
33 arquivos alterados com 1164 adições e 33 exclusões
  1. 14 4
      .env.template
  2. 1 0
      migrations/mysql/2019-11-17-011009_add_email_verification/down.sql
  3. 5 0
      migrations/mysql/2019-11-17-011009_add_email_verification/up.sql
  4. 1 0
      migrations/postgresql/2019-11-17-011009_add_email_verification/down.sql
  5. 5 0
      migrations/postgresql/2019-11-17-011009_add_email_verification/up.sql
  6. 1 0
      migrations/sqlite/2019-11-17-011009_add_email_verification/down.sql
  7. 5 0
      migrations/sqlite/2019-11-17-011009_add_email_verification/up.sql
  8. 170 5
      src/api/core/accounts.rs
  9. 2 18
      src/api/core/two_factor/email.rs
  10. 29 0
      src/api/identity.rs
  11. 58 0
      src/auth.rs
  12. 11 0
      src/config.rs
  13. 16 0
      src/crypto.rs
  14. 2 1
      src/db/models/device.rs
  15. 11 1
      src/db/models/user.rs
  16. 5 0
      src/db/schemas/mysql/schema.rs
  17. 6 1
      src/db/schemas/postgresql/schema.rs
  18. 5 0
      src/db/schemas/sqlite/schema.rs
  19. 80 1
      src/mail.rs
  20. 6 0
      src/static/templates/email/change_email.hbs
  21. 129 0
      src/static/templates/email/change_email.html.hbs
  22. 12 0
      src/static/templates/email/delete_account.hbs
  23. 137 0
      src/static/templates/email/delete_account.html.hbs
  24. 6 2
      src/static/templates/email/pw_hint_none.hbs
  25. 5 0
      src/static/templates/email/pw_hint_none.html.hbs
  26. 2 0
      src/static/templates/email/pw_hint_some.hbs
  27. 5 0
      src/static/templates/email/pw_hint_some.html.hbs
  28. 12 0
      src/static/templates/email/verify_email.hbs
  29. 137 0
      src/static/templates/email/verify_email.html.hbs
  30. 8 0
      src/static/templates/email/welcome.hbs
  31. 129 0
      src/static/templates/email/welcome.html.hbs
  32. 12 0
      src/static/templates/email/welcome_must_verify.hbs
  33. 137 0
      src/static/templates/email/welcome_must_verify.html.hbs

+ 14 - 4
.env.template

@@ -95,12 +95,22 @@
 ## Controls if new users can register
 # SIGNUPS_ALLOWED=true
 
+## Controls if new users need to verify their email address upon registration
+## Note that setting this option to true prevents logins until the email address has been verified!
+## The welcome email will include a verification link, and login attempts will periodically
+## trigger another verification email to be sent.
+# SIGNUPS_VERIFY=false
+
+## If SIGNUPS_VERIFY is set to true, this limits how many seconds after the last time
+## an email verification link has been sent another verification email will be sent
+# SIGNUPS_VERIFY_RESEND_TIME=3600
+
+## If SIGNUPS_VERIFY is set to true, this limits how many times an email verification
+## email will be re-sent upon an attempted login.
+# SIGNUPS_VERIFY_RESEND_LIMIT=6
+
 ## Controls if new users from a list of comma-separated domains can register
 ## even if SIGNUPS_ALLOWED is set to false
-##
-## WARNING: There is currently no validation that prevents anyone from
-##          signing up with any made-up email address from one of these
-##          whitelisted domains!
 # SIGNUPS_DOMAINS_WHITELIST=example.com,example.net,example.org
 
 ## Token for the admin interface, preferably use a long random string

+ 1 - 0
migrations/mysql/2019-11-17-011009_add_email_verification/down.sql

@@ -0,0 +1 @@
+

+ 5 - 0
migrations/mysql/2019-11-17-011009_add_email_verification/up.sql

@@ -0,0 +1,5 @@
+ALTER TABLE users ADD COLUMN verified_at DATETIME DEFAULT NULL;
+ALTER TABLE users ADD COLUMN last_verifying_at DATETIME DEFAULT NULL;
+ALTER TABLE users ADD COLUMN login_verify_count INTEGER NOT NULL DEFAULT 0;
+ALTER TABLE users ADD COLUMN email_new VARCHAR(255) DEFAULT NULL;
+ALTER TABLE users ADD COLUMN email_new_token VARCHAR(16) DEFAULT NULL;

+ 1 - 0
migrations/postgresql/2019-11-17-011009_add_email_verification/down.sql

@@ -0,0 +1 @@
+

+ 5 - 0
migrations/postgresql/2019-11-17-011009_add_email_verification/up.sql

@@ -0,0 +1,5 @@
+ALTER TABLE users ADD COLUMN verified_at TIMESTAMP DEFAULT NULL;
+ALTER TABLE users ADD COLUMN last_verifying_at TIMESTAMP DEFAULT NULL;
+ALTER TABLE users ADD COLUMN login_verify_count INTEGER NOT NULL DEFAULT 0;
+ALTER TABLE users ADD COLUMN email_new VARCHAR(255) DEFAULT NULL;
+ALTER TABLE users ADD COLUMN email_new_token VARCHAR(16) DEFAULT NULL;

+ 1 - 0
migrations/sqlite/2019-11-17-011009_add_email_verification/down.sql

@@ -0,0 +1 @@
+

+ 5 - 0
migrations/sqlite/2019-11-17-011009_add_email_verification/up.sql

@@ -0,0 +1,5 @@
+ALTER TABLE users ADD COLUMN verified_at DATETIME DEFAULT NULL;
+ALTER TABLE users ADD COLUMN last_verifying_at DATETIME DEFAULT NULL;
+ALTER TABLE users ADD COLUMN login_verify_count INTEGER NOT NULL DEFAULT 0;
+ALTER TABLE users ADD COLUMN email_new TEXT DEFAULT NULL;
+ALTER TABLE users ADD COLUMN email_new_token TEXT DEFAULT NULL;

+ 170 - 5
src/api/core/accounts.rs

@@ -1,11 +1,13 @@
 use rocket_contrib::json::Json;
+use chrono::Utc;
 
 use crate::db::models::*;
 use crate::db::DbConn;
 
 use crate::api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType};
-use crate::auth::{decode_invite, Headers};
+use crate::auth::{decode_invite, decode_delete, decode_verify_email, Headers};
 use crate::mail;
+use crate::crypto;
 
 use crate::CONFIG;
 
@@ -25,6 +27,10 @@ pub fn routes() -> Vec<Route> {
         post_sstamp,
         post_email_token,
         post_email,
+        post_verify_email,
+        post_verify_email_token,
+        post_delete_recover,
+        post_delete_recover_token,
         delete_account,
         post_delete_account,
         revision_date,
@@ -126,6 +132,20 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
         user.public_key = Some(keys.PublicKey);
     }
 
+    if CONFIG.mail_enabled() {
+        if CONFIG.signups_verify() {
+            if let Err(e) = mail::send_welcome_must_verify(&user.email, &user.uuid) {
+                error!("Error sending welcome email: {:#?}", e);
+            }
+
+            user.last_verifying_at = Some(user.created_at);
+        } else {
+            if let Err(e) = mail::send_welcome(&user.email) {
+                error!("Error sending welcome email: {:#?}", e);
+            }
+        }
+    }
+
     user.save(&conn)
 }
 
@@ -341,8 +361,9 @@ struct EmailTokenData {
 #[post("/accounts/email-token", data = "<data>")]
 fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, conn: DbConn) -> EmptyResult {
     let data: EmailTokenData = data.into_inner().data;
+    let mut user = headers.user;
 
-    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
+    if !user.check_valid_password(&data.MasterPasswordHash) {
         err!("Invalid password")
     }
 
@@ -350,7 +371,21 @@ fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, conn: Db
         err!("Email already in use");
     }
 
-    Ok(())
+    if !CONFIG.signups_allowed() && !CONFIG.can_signup_user(&data.NewEmail) {
+        err!("Email cannot be changed to this address");
+    }
+
+    let token = crypto::generate_token(6)?;
+
+    if CONFIG.mail_enabled() {
+        if let Err(e) = mail::send_change_email(&data.NewEmail, &token) {
+            error!("Error sending change-email email: {:#?}", e);
+        }
+    }
+
+    user.email_new = Some(data.NewEmail);
+    user.email_new_token = Some(token);
+    user.save(&conn)
 }
 
 #[derive(Deserialize)]
@@ -361,8 +396,7 @@ struct ChangeEmailData {
 
     Key: String,
     NewMasterPasswordHash: String,
-    #[serde(rename = "Token")]
-    _Token: NumberOrString,
+    Token: NumberOrString,
 }
 
 #[post("/accounts/email", data = "<data>")]
@@ -378,7 +412,32 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
         err!("Email already in use");
     }
 
+    match user.email_new {
+        Some(ref val) => {
+            if *val != data.NewEmail.to_string() {
+                err!("Email change mismatch");
+            }
+        },
+        None => err!("No email change pending"),
+    }
+
+    if CONFIG.mail_enabled() {
+        // Only check the token if we sent out an email...
+        match user.email_new_token {
+            Some(ref val) =>
+                if *val != data.Token.into_string() {
+                    err!("Token mismatch");
+                }
+            None => err!("No email change pending"),
+        }
+        user.verified_at = Some(Utc::now().naive_utc());
+    } else {
+        user.verified_at = None;
+    }
+
     user.email = data.NewEmail;
+    user.email_new = None;
+    user.email_new_token = None;
 
     user.set_password(&data.NewMasterPasswordHash);
     user.akey = data.Key;
@@ -386,6 +445,112 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
     user.save(&conn)
 }
 
+#[post("/accounts/verify-email")]
+fn post_verify_email(headers: Headers, _conn: DbConn) -> EmptyResult {
+    let user = headers.user;
+
+    if !CONFIG.mail_enabled() {
+        err!("Cannot verify email address");
+    }
+
+    if let Err(e) = mail::send_verify_email(&user.email, &user.uuid) {
+        error!("Error sending delete account email: {:#?}", e);
+    }
+
+    Ok(())
+}
+
+#[derive(Deserialize)]
+#[allow(non_snake_case)]
+struct VerifyEmailTokenData {
+    UserId: String,
+    Token: String,
+}
+
+#[post("/accounts/verify-email-token", data = "<data>")]
+fn post_verify_email_token(data: JsonUpcase<VerifyEmailTokenData>, conn: DbConn) -> EmptyResult {
+    let data: VerifyEmailTokenData = data.into_inner().data;
+
+    let mut user = match User::find_by_uuid(&data.UserId, &conn) {
+        Some(user) => user,
+        None => err!("User doesn't exist"),
+    };
+
+    let claims = match decode_verify_email(&data.Token) {
+        Ok(claims) => claims,
+        Err(_) => err!("Invalid claim"),
+    };
+    
+    if claims.sub != user.uuid {
+       err!("Invalid claim");
+    }
+    
+    user.verified_at = Some(Utc::now().naive_utc());
+    user.last_verifying_at = None;
+    user.login_verify_count = 0;
+    if let Err(e) = user.save(&conn) {
+        error!("Error saving email verification: {:#?}", e);
+    }
+
+    Ok(())
+}
+
+#[derive(Deserialize)]
+#[allow(non_snake_case)]
+struct DeleteRecoverData {
+    Email: String,
+}
+
+#[post("/accounts/delete-recover", data="<data>")]
+fn post_delete_recover(data: JsonUpcase<DeleteRecoverData>, conn: DbConn) -> EmptyResult {
+    let data: DeleteRecoverData = data.into_inner().data;
+
+    let user = User::find_by_mail(&data.Email, &conn);
+
+    if CONFIG.mail_enabled() {
+        if let Some(user) = user {
+            if let Err(e) = mail::send_delete_account(&user.email, &user.uuid) {
+                error!("Error sending delete account email: {:#?}", e);
+            }
+        }
+        Ok(())
+    } else {
+        // We don't support sending emails, but we shouldn't allow anybody
+        // to delete accounts without at least logging in... And if the user
+        // cannot remember their password then they will need to contact
+        // the administrator to delete it...
+        err!("Please contact the administrator to delete your account");
+    }
+}
+
+#[derive(Deserialize)]
+#[allow(non_snake_case)]
+struct DeleteRecoverTokenData {
+    UserId: String,
+    Token: String,
+}
+
+#[post("/accounts/delete-recover-token", data="<data>")]
+fn post_delete_recover_token(data: JsonUpcase<DeleteRecoverTokenData>, conn: DbConn) -> EmptyResult {
+    let data: DeleteRecoverTokenData = data.into_inner().data;
+
+    let user = match User::find_by_uuid(&data.UserId, &conn) {
+        Some(user) => user,
+        None => err!("User doesn't exist"),
+    };
+
+    let claims = match decode_delete(&data.Token) {
+        Ok(claims) => claims,
+        Err(_) => err!("Invalid claim"),
+    };
+    
+    if claims.sub != user.uuid {
+       err!("Invalid claim");
+    }
+    
+    user.delete(&conn)
+}
+
 #[post("/accounts/delete", data = "<data>")]
 fn post_delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult {
     delete_account(data, headers, conn)

+ 2 - 18
src/api/core/two_factor/email.rs

@@ -66,7 +66,7 @@ pub fn send_token(user_uuid: &str, conn: &DbConn) -> EmptyResult {
     let type_ = TwoFactorType::Email as i32;
     let mut twofactor = TwoFactor::find_by_user_and_type(user_uuid, type_, &conn)?;
 
-    let generated_token = generate_token(CONFIG.email_token_size())?;
+    let generated_token = crypto::generate_token(CONFIG.email_token_size())?;
 
     let mut twofactor_data = EmailTokenData::from_json(&twofactor.data)?;
     twofactor_data.set_token(generated_token);
@@ -109,22 +109,6 @@ struct SendEmailData {
     MasterPasswordHash: String,
 }
 
-
-fn generate_token(token_size: u32) -> Result<String, Error> {
-    if token_size > 19 {
-        err!("Generating token failed")
-    }
-
-    // 8 bytes to create an u64 for up to 19 token digits
-    let bytes = crypto::get_random(vec![0; 8]);
-    let mut bytes_array = [0u8; 8];
-    bytes_array.copy_from_slice(&bytes);
-
-    let number = u64::from_be_bytes(bytes_array) % 10u64.pow(token_size);
-    let token = format!("{:0size$}", number, size = token_size as usize);
-    Ok(token)
-}
-
 /// Send a verification email to the specified email address to check whether it exists/belongs to user.
 #[post("/two-factor/send-email", data = "<data>")]
 fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -> EmptyResult {
@@ -145,7 +129,7 @@ fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -
         tf.delete(&conn)?;
     }
 
-    let generated_token = generate_token(CONFIG.email_token_size())?;
+    let generated_token = crypto::generate_token(CONFIG.email_token_size())?;
     let twofactor_data = EmailTokenData::new(data.Email, generated_token);
 
     // Uses EmailVerificationChallenge as type to show that it's not verified yet.

+ 29 - 0
src/api/identity.rs

@@ -3,6 +3,7 @@ use rocket::request::{Form, FormItems, FromForm};
 use rocket::Route;
 use rocket_contrib::json::Json;
 use serde_json::Value;
+use chrono::Utc;
 
 use crate::api::core::two_factor::email::EmailTokenData;
 use crate::api::core::two_factor::{duo, email, yubikey};
@@ -96,6 +97,34 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult
         )
     }
 
+    if !user.verified_at.is_some() && CONFIG.mail_enabled() && CONFIG.signups_verify() {
+        let now = Utc::now().naive_utc();
+        if user.last_verifying_at.is_none() || now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds() > CONFIG.signups_verify_resend_time() as i64 {
+            let resend_limit = CONFIG.signups_verify_resend_limit() as i32;
+            if resend_limit == 0 || user.login_verify_count < resend_limit {
+                // We want to send another email verification if we require signups to verify
+                // their email address, and we haven't sent them a reminder in a while...
+                let mut user = user;
+                user.last_verifying_at = Some(now);
+                user.login_verify_count = user.login_verify_count + 1;
+
+                if let Err(e) = user.save(&conn) {
+                    error!("Error updating user: {:#?}", e);
+                }
+
+                if let Err(e) = mail::send_verify_email(&user.email, &user.uuid) {
+                    error!("Error auto-sending email verification email: {:#?}", e);
+                }
+            }
+        }
+
+        // We still want the login to fail until they actually verified the email address
+        err!(
+            "Please verify your email before trying again.",
+            format!("IP: {}. Username: {}.", ip.ip, username)
+        )
+    }
+
     let (mut device, new_device) = get_device(&data, &conn, &user);
 
     let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &conn)?;

+ 58 - 0
src/auth.rs

@@ -18,6 +18,8 @@ lazy_static! {
     static ref JWT_HEADER: Header = Header::new(JWT_ALGORITHM);
     pub static ref JWT_LOGIN_ISSUER: String = format!("{}|login", CONFIG.domain());
     pub static ref JWT_INVITE_ISSUER: String = format!("{}|invite", CONFIG.domain());
+    pub static ref JWT_DELETE_ISSUER: String = format!("{}|delete", CONFIG.domain());
+    pub static ref JWT_VERIFYEMAIL_ISSUER: String = format!("{}|verifyemail", CONFIG.domain());
     pub static ref JWT_ADMIN_ISSUER: String = format!("{}|admin", CONFIG.domain());
     static ref PRIVATE_RSA_KEY: Vec<u8> = match read_file(&CONFIG.private_rsa_key()) {
         Ok(key) => key,
@@ -62,6 +64,14 @@ pub fn decode_invite(token: &str) -> Result<InviteJWTClaims, Error> {
     decode_jwt(token, JWT_INVITE_ISSUER.to_string())
 }
 
+pub fn decode_delete(token: &str) -> Result<DeleteJWTClaims, Error> {
+    decode_jwt(token, JWT_DELETE_ISSUER.to_string())
+}
+
+pub fn decode_verify_email(token: &str) -> Result<VerifyEmailJWTClaims, Error> {
+    decode_jwt(token, JWT_VERIFYEMAIL_ISSUER.to_string())
+}
+
 pub fn decode_admin(token: &str) -> Result<AdminJWTClaims, Error> {
     decode_jwt(token, JWT_ADMIN_ISSUER.to_string())
 }
@@ -134,6 +144,54 @@ pub fn generate_invite_claims(
     }
 }
 
+#[derive(Debug, Serialize, Deserialize)]
+pub struct DeleteJWTClaims {
+    // Not before
+    pub nbf: i64,
+    // Expiration time
+    pub exp: i64,
+    // Issuer
+    pub iss: String,
+    // Subject
+    pub sub: String,
+}
+
+pub fn generate_delete_claims(
+    uuid: String,
+) -> DeleteJWTClaims {
+    let time_now = Utc::now().naive_utc();
+    DeleteJWTClaims {
+        nbf: time_now.timestamp(),
+        exp: (time_now + Duration::days(5)).timestamp(),
+        iss: JWT_DELETE_ISSUER.to_string(),
+        sub: uuid,
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct VerifyEmailJWTClaims {
+    // Not before
+    pub nbf: i64,
+    // Expiration time
+    pub exp: i64,
+    // Issuer
+    pub iss: String,
+    // Subject
+    pub sub: String,
+}
+
+pub fn generate_verify_email_claims(
+    uuid: String,
+) -> DeleteJWTClaims {
+    let time_now = Utc::now().naive_utc();
+    DeleteJWTClaims {
+        nbf: time_now.timestamp(),
+        exp: (time_now + Duration::days(5)).timestamp(),
+        iss: JWT_VERIFYEMAIL_ISSUER.to_string(),
+        sub: uuid,
+    }
+}
+
 #[derive(Debug, Serialize, Deserialize)]
 pub struct AdminJWTClaims {
     // Not before

+ 11 - 0
src/config.rs

@@ -243,6 +243,12 @@ make_config! {
         disable_icon_download:  bool,   true,   def,    false;
         /// Allow new signups |> Controls if new users can register. Note that while this is disabled, users could still be invited
         signups_allowed:        bool,   true,   def,    true;
+        /// Require email verification on signups. This will prevent logins from succeeding until the address has been verified
+        signups_verify:         bool,   true,   def,    false;
+        /// If signups require email verification, automatically re-send verification email if it hasn't been sent for a while (in seconds)
+        signups_verify_resend_time: u64, true,  def,    3_600;
+        /// If signups require email verification, limit how many emails are automatically sent when login is attempted (0 means no limit)
+        signups_verify_resend_limit: u32, true, def,    6;
         /// Allow signups only from this list of comma-separated domains
         signups_domains_whitelist: String, true, def,   "".to_string();
         /// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are disabled
@@ -595,6 +601,8 @@ fn load_templates(path: &str) -> Handlebars {
     }
 
     // First register default templates here
+    reg!("email/change_email", ".html");
+    reg!("email/delete_account", ".html");
     reg!("email/invite_accepted", ".html");
     reg!("email/invite_confirmed", ".html");
     reg!("email/new_device_logged_in", ".html");
@@ -602,6 +610,9 @@ fn load_templates(path: &str) -> Handlebars {
     reg!("email/pw_hint_some", ".html");
     reg!("email/send_org_invite", ".html");
     reg!("email/twofactor_email", ".html");
+    reg!("email/verify_email", ".html");
+    reg!("email/welcome", ".html");
+    reg!("email/welcome_must_verify", ".html");
 
     reg!("admin/base");
     reg!("admin/login");

+ 16 - 0
src/crypto.rs

@@ -4,6 +4,7 @@
 
 use ring::{digest, hmac, pbkdf2};
 use std::num::NonZeroU32;
+use crate::error::Error;
 
 static DIGEST_ALG: &digest::Algorithm = &digest::SHA256;
 const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN;
@@ -52,6 +53,21 @@ pub fn get_random(mut array: Vec<u8>) -> Vec<u8> {
     array
 }
 
+pub fn generate_token(token_size: u32) -> Result<String, Error> {
+    if token_size > 19 {
+        err!("Generating token failed")
+    }
+
+    // 8 bytes to create an u64 for up to 19 token digits
+    let bytes = get_random(vec![0; 8]);
+    let mut bytes_array = [0u8; 8];
+    bytes_array.copy_from_slice(&bytes);
+
+    let number = u64::from_be_bytes(bytes_array) % 10u64.pow(token_size);
+    let token = format!("{:0size$}", number, size = token_size as usize);
+    Ok(token)
+}
+
 //
 // Constant time compare
 //

+ 2 - 1
src/db/models/device.rs

@@ -1,6 +1,7 @@
 use chrono::{NaiveDateTime, Utc};
 
 use super::User;
+use crate::CONFIG;
 
 #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
 #[table_name = "devices"]
@@ -87,7 +88,7 @@ impl Device {
             premium: true,
             name: user.name.to_string(),
             email: user.email.to_string(),
-            email_verified: true,
+            email_verified: !CONFIG.mail_enabled() || user.verified_at.is_some(),
 
             orgowner,
             orgadmin,

+ 11 - 1
src/db/models/user.rs

@@ -11,8 +11,13 @@ pub struct User {
     pub uuid: String,
     pub created_at: NaiveDateTime,
     pub updated_at: NaiveDateTime,
+    pub verified_at: Option<NaiveDateTime>,
+    pub last_verifying_at: Option<NaiveDateTime>,
+    pub login_verify_count: i32,
 
     pub email: String,
+    pub email_new: Option<String>,
+    pub email_new_token: Option<String>,
     pub name: String,
 
     pub password_hash: Vec<u8>,
@@ -56,9 +61,14 @@ impl User {
             uuid: crate::util::get_uuid(),
             created_at: now,
             updated_at: now,
+            verified_at: None,
+            last_verifying_at: None,
+            login_verify_count: 0,
             name: email.clone(),
             email,
             akey: String::new(),
+            email_new: None,
+            email_new_token: None,
 
             password_hash: Vec::new(),
             salt: crypto::get_random_64(),
@@ -135,7 +145,7 @@ impl User {
             "Id": self.uuid,
             "Name": self.name,
             "Email": self.email,
-            "EmailVerified": true,
+            "EmailVerified": !CONFIG.mail_enabled() || self.verified_at.is_some(),
             "Premium": true,
             "MasterPasswordHint": self.password_hint,
             "Culture": "en-US",

+ 5 - 0
src/db/schemas/mysql/schema.rs

@@ -101,7 +101,12 @@ table! {
         uuid -> Varchar,
         created_at -> Datetime,
         updated_at -> Datetime,
+        verified_at -> Nullable<Datetime>,
+        last_verifying_at -> Nullable<Datetime>,
+        login_verify_count -> Integer,
         email -> Varchar,
+        email_new -> Nullable<Varchar>,
+        email_new_token -> Nullable<Varchar>,
         name -> Text,
         password_hash -> Blob,
         salt -> Blob,

+ 6 - 1
src/db/schemas/postgresql/schema.rs

@@ -101,7 +101,12 @@ table! {
         uuid -> Text,
         created_at -> Timestamp,
         updated_at -> Timestamp,
+        verified_at -> Nullable<Timestamp>,
+        last_verifying_at -> Nullable<Timestamp>,
+        login_verify_count -> Integer,
         email -> Text,
+        email_new -> Nullable<Text>,
+        email_new_token -> Nullable<Text>,
         name -> Text,
         password_hash -> Binary,
         salt -> Binary,
@@ -170,4 +175,4 @@ allow_tables_to_appear_in_same_query!(
     users,
     users_collections,
     users_organizations,
-);
+);

+ 5 - 0
src/db/schemas/sqlite/schema.rs

@@ -101,7 +101,12 @@ table! {
         uuid -> Text,
         created_at -> Timestamp,
         updated_at -> Timestamp,
+        verified_at -> Nullable<Timestamp>,
+        last_verifying_at -> Nullable<Timestamp>,
+        login_verify_count -> Integer,
         email -> Text,
+        email_new -> Nullable<Text>,
+        email_new_token -> Nullable<Text>,
         name -> Text,
         password_hash -> Binary,
         salt -> Binary,

+ 80 - 1
src/mail.rs

@@ -8,7 +8,7 @@ use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
 use quoted_printable::encode_to_str;
 
 use crate::api::EmptyResult;
-use crate::auth::{encode_jwt, generate_invite_claims};
+use crate::auth::{encode_jwt, generate_invite_claims, generate_delete_claims, generate_verify_email_claims};
 use crate::error::Error;
 use crate::CONFIG;
 use chrono::NaiveDateTime;
@@ -95,6 +95,73 @@ pub fn send_password_hint(address: &str, hint: Option<String>) -> EmptyResult {
     send_email(&address, &subject, &body_html, &body_text)
 }
 
+pub fn send_delete_account(address: &str, uuid: &str) -> EmptyResult {
+    let claims = generate_delete_claims(
+        uuid.to_string(),
+    );
+    let delete_token = encode_jwt(&claims);
+
+    let (subject, body_html, body_text) = get_text(
+        "email/delete_account",
+        json!({
+            "url": CONFIG.domain(),
+            "user_id": uuid,
+            "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
+            "token": delete_token,
+        }),
+    )?;
+
+    send_email(&address, &subject, &body_html, &body_text)
+}
+
+pub fn send_verify_email(address: &str, uuid: &str) -> EmptyResult {
+    let claims = generate_verify_email_claims(
+        uuid.to_string(),
+    );
+    let verify_email_token = encode_jwt(&claims);
+
+    let (subject, body_html, body_text) = get_text(
+        "email/verify_email",
+        json!({
+            "url": CONFIG.domain(),
+            "user_id": uuid,
+            "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
+            "token": verify_email_token,
+        }),
+    )?;
+
+    send_email(&address, &subject, &body_html, &body_text)
+}
+
+pub fn send_welcome(address: &str) -> EmptyResult {
+    let (subject, body_html, body_text) = get_text(
+        "email/welcome",
+        json!({
+            "url": CONFIG.domain(),
+        }),
+    )?;
+
+    send_email(&address, &subject, &body_html, &body_text)
+}
+
+pub fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult {
+    let claims = generate_verify_email_claims(
+        uuid.to_string(),
+    );
+    let verify_email_token = encode_jwt(&claims);
+
+    let (subject, body_html, body_text) = get_text(
+        "email/welcome_must_verify",
+        json!({
+            "url": CONFIG.domain(),
+            "user_id": uuid,
+            "token": verify_email_token,
+        }),
+    )?;
+
+    send_email(&address, &subject, &body_html, &body_text)
+}
+
 pub fn send_invite(
     address: &str,
     uuid: &str,
@@ -183,6 +250,18 @@ pub fn send_token(address: &str, token: &str) -> EmptyResult {
     send_email(&address, &subject, &body_html, &body_text)
 }
 
+pub fn send_change_email(address: &str, token: &str) -> EmptyResult {
+    let (subject, body_html, body_text) = get_text(
+        "email/change_email",
+        json!({
+            "url": CONFIG.domain(),
+            "token": token,
+        }),
+    )?;
+
+    send_email(&address, &subject, &body_html, &body_text)
+}
+
 fn send_email(address: &str, subject: &str, body_html: &str, body_text: &str) -> EmptyResult {
     let html = PartBuilder::new()
         .body(encode_to_str(body_html))

+ 6 - 0
src/static/templates/email/change_email.hbs

@@ -0,0 +1,6 @@
+Your Email Change
+<!---------------->
+<html>
+<p>To finalize changing your email address enter the following code in web vault: <b>{{token}}</b></p>
+<p>If you did not try to change an email address, you can safely ignore this email.</p>
+</html>

+ 129 - 0
src/static/templates/email/change_email.html.hbs

@@ -0,0 +1,129 @@
+Your Email Change
+<!---------------->
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+   <head>
+      <meta name="viewport" content="width=device-width" />
+      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+      <title>Bitwarden_rs</title>
+   </head>
+   <body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6">
+      <style type="text/css">
+          body {
+         margin: 0;
+         font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+         box-sizing: border-box;
+         font-size: 16px;
+         color: #333;
+         line-height: 25px;
+         -webkit-font-smoothing: antialiased;
+         -webkit-text-size-adjust: none;
+         }
+         body * {
+         margin: 0;
+         font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+         box-sizing: border-box;
+         font-size: 16px;
+         color: #333;
+         line-height: 25px;
+         -webkit-font-smoothing: antialiased;
+         -webkit-text-size-adjust: none;
+         }
+         img {
+         max-width: 100%;
+         border: none;
+         }
+         body {
+         -webkit-font-smoothing: antialiased;
+         -webkit-text-size-adjust: none;
+         width: 100% !important;
+         height: 100%;
+         line-height: 25px;
+         }
+         body {
+         background-color: #f6f6f6;
+         }
+         @media only screen and (max-width: 600px) {
+         body {
+         padding: 0 !important;
+         }
+         .container {
+         padding: 0 !important;
+         width: 100% !important;
+         }
+         .container-table {
+         padding: 0 !important;
+         width: 100% !important;
+         }
+         .content {
+         padding: 0 0 10px 0 !important;
+         }
+         .content-wrap {
+         padding: 10px !important;
+         }
+         .invoice {
+         width: 100% !important;
+         }
+         .main {
+         border-right: none !important;
+         border-left: none !important;
+         border-radius: 0 !important;
+         }
+         .logo {
+         padding-top: 10px !important;
+         }
+         .footer {
+         margin-top: 10px !important;
+         }
+         .indented {
+         padding-left: 10px;
+         }
+         }
+      </style>
+      <table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6">
+         <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+            <td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center">
+                <img src="{{url}}/bwrs_static/logo-gray.png" alt="" width="250" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" />
+            </td>
+         </tr>
+         <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+            <td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
+               <table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
+                  <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+                     <td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
+                        <table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
+                           <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                              <td class="content-wrap" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 20px; -webkit-text-size-adjust: none;" valign="top">
+                                 <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                       <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
+                                          To finalize changing your email address enter the following code in web vault: <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{token}}</b>
+                                       </td>
+                                    </tr>
+                                    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                       <td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
+                                         If you did not try to change an email address, you can safely ignore this email. 
+                                       </td>
+                                    </tr>
+                                 </table>
+                              </td>
+                           </tr>
+                        </table>
+                        <table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
+                           <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+                              <td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
+                                 <table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
+                                    <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+                                        <td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_static/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
+                                    </tr>
+                                 </table>
+                              </td>
+                           </tr>
+                        </table>
+                     </td>
+                  </tr>
+               </table>
+            </td>
+         </tr>
+      </table>
+   </body>
+</html>

+ 12 - 0
src/static/templates/email/delete_account.hbs

@@ -0,0 +1,12 @@
+Delete Your Account
+<!---------------->
+<html>
+<p>
+click the link below to delete your account.
+<br>
+<br>
+<a href="{{url}}/#/verify-recover-delete?userId={{user_id}}&token={{token}}&email={{email}}">
+Delete Your Account</a>
+</p>
+<p>If you did not request this email to delete your account, you can safely ignore this email.</p>
+</html>

+ 137 - 0
src/static/templates/email/delete_account.html.hbs

@@ -0,0 +1,137 @@
+Delete Your Account
+<!---------------->
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+   <head>
+      <meta name="viewport" content="width=device-width" />
+      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+      <title>Bitwarden_rs</title>
+   </head>
+   <body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6">
+      <style type="text/css">
+          body {
+         margin: 0;
+         font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+         box-sizing: border-box;
+         font-size: 16px;
+         color: #333;
+         line-height: 25px;
+         -webkit-font-smoothing: antialiased;
+         -webkit-text-size-adjust: none;
+         }
+         body * {
+         margin: 0;
+         font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+         box-sizing: border-box;
+         font-size: 16px;
+         color: #333;
+         line-height: 25px;
+         -webkit-font-smoothing: antialiased;
+         -webkit-text-size-adjust: none;
+         }
+         img {
+         max-width: 100%;
+         border: none;
+         }
+         body {
+         -webkit-font-smoothing: antialiased;
+         -webkit-text-size-adjust: none;
+         width: 100% !important;
+         height: 100%;
+         line-height: 25px;
+         }
+         body {
+         background-color: #f6f6f6;
+         }
+         @media only screen and (max-width: 600px) {
+         body {
+         padding: 0 !important;
+         }
+         .container {
+         padding: 0 !important;
+         width: 100% !important;
+         }
+         .container-table {
+         padding: 0 !important;
+         width: 100% !important;
+         }
+         .content {
+         padding: 0 0 10px 0 !important;
+         }
+         .content-wrap {
+         padding: 10px !important;
+         }
+         .invoice {
+         width: 100% !important;
+         }
+         .main {
+         border-right: none !important;
+         border-left: none !important;
+         border-radius: 0 !important;
+         }
+         .logo {
+         padding-top: 10px !important;
+         }
+         .footer {
+         margin-top: 10px !important;
+         }
+         .indented {
+         padding-left: 10px;
+         }
+         }
+      </style>
+      <table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6">
+         <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+            <td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center">
+                <img src="{{url}}/bwrs_static/logo-gray.png" alt="" width="250" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" />
+            </td>
+         </tr>
+         <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+            <td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
+               <table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
+                  <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+                     <td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
+                        <table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
+                           <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                              <td class="content-wrap" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 20px; -webkit-text-size-adjust: none;" valign="top">
+                                 <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                       <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
+                                          click the link below to delete your account.
+                                       </td>
+                                    </tr>
+                                    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                       <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
+                                          <a href="{{url}}/#/verify-recover-delete?userId={{user_id}}&token={{token}}&email={{email}}"
+                                             clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                          Delete Your Account
+                                          </a>
+                                       </td>
+                                    </tr>
+                                    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                       <td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
+                                          If you did not request this email to delete your account, you can safely ignore this email.
+                                       </td>
+                                    </tr>
+                                 </table>
+                              </td>
+                           </tr>
+                        </table>
+                        <table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
+                           <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+                              <td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
+                                 <table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
+                                    <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+                                        <td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_static/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
+                                    </tr>
+                                 </table>
+                              </td>
+                           </tr>
+                        </table>
+                     </td>
+                  </tr>
+               </table>
+            </td>
+         </tr>
+      </table>
+   </body>
+</html>

+ 6 - 2
src/static/templates/email/pw_hint_none.hbs

@@ -1,3 +1,7 @@
-Sorry, you have no password hint...
+Your master password hint
 <!---------------->
-Sorry, you have not specified any password hint...
+You (or someone) recently requested your master password hint. Unfortunately, your account does not have a master password hint.
+
+If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to <a href="{{url}}/#/recover-delete">delete the account</a> so that you can register again and start over. All data associated with your account will be deleted.
+
+If you did not request your master password hint you can safely ignore this email.

+ 5 - 0
src/static/templates/email/pw_hint_none.html.hbs

@@ -99,6 +99,11 @@ Sorry, you have no password hint...
                                           You (or someone) recently requested your master password hint. Unfortunately, your account does not have a master password hint. <br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
                                        </td>
                                     </tr>
+                                    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                       <td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
+                                          If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to <a href="{{url}}/#/recover-delete">delete the account</a> so that you can register again and start over. All data associated with your account will be deleted.
+                                       </td>
+                                    </tr>
                                     <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
                                        <td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
                                           If you did not request your master password hint you can safely ignore this email.

+ 2 - 0
src/static/templates/email/pw_hint_some.hbs

@@ -5,4 +5,6 @@ You (or someone) recently requested your master password hint.
 Your hint is: "{{hint}}"
 Log in: <a href="{{url}}">Web Vault</a>
 
+If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to <a href="{{url}}/#/recover-delete">delete the account</a> so that you can register again and start over. All data associated with your account will be deleted.
+
 If you did not request your master password hint you can safely ignore this email.

+ 5 - 0
src/static/templates/email/pw_hint_some.html.hbs

@@ -105,6 +105,11 @@ Your master password hint
                                           Log in: <a href="{{url}}">Web Vault</a>
                                        </td>
                                     </tr>
+                                    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                       <td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
+                                          If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to <a href="{{url}}/#/recover-delete">delete the account</a> so that you can register again and start over. All data associated with your account will be deleted.
+                                       </td>
+                                    </tr>
                                     <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
                                        <td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
                                           If you did not request your master password hint you can safely ignore this email.

+ 12 - 0
src/static/templates/email/verify_email.hbs

@@ -0,0 +1,12 @@
+Verify Your Email
+<!---------------->
+<html>
+<p>
+Verify this email address for your account by clicking the link below.
+<br>
+<br>
+<a href="{{url}}/#/verify-email/?userId={{user_id}}&token={{token}}">
+Verify Email Address Now</a>
+</p>
+<p>If you did not request to verify your account, you can safely ignore this email.</p>
+</html>

+ 137 - 0
src/static/templates/email/verify_email.html.hbs

@@ -0,0 +1,137 @@
+Verify Your Email
+<!---------------->
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+   <head>
+      <meta name="viewport" content="width=device-width" />
+      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+      <title>Bitwarden_rs</title>
+   </head>
+   <body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6">
+      <style type="text/css">
+          body {
+         margin: 0;
+         font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+         box-sizing: border-box;
+         font-size: 16px;
+         color: #333;
+         line-height: 25px;
+         -webkit-font-smoothing: antialiased;
+         -webkit-text-size-adjust: none;
+         }
+         body * {
+         margin: 0;
+         font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+         box-sizing: border-box;
+         font-size: 16px;
+         color: #333;
+         line-height: 25px;
+         -webkit-font-smoothing: antialiased;
+         -webkit-text-size-adjust: none;
+         }
+         img {
+         max-width: 100%;
+         border: none;
+         }
+         body {
+         -webkit-font-smoothing: antialiased;
+         -webkit-text-size-adjust: none;
+         width: 100% !important;
+         height: 100%;
+         line-height: 25px;
+         }
+         body {
+         background-color: #f6f6f6;
+         }
+         @media only screen and (max-width: 600px) {
+         body {
+         padding: 0 !important;
+         }
+         .container {
+         padding: 0 !important;
+         width: 100% !important;
+         }
+         .container-table {
+         padding: 0 !important;
+         width: 100% !important;
+         }
+         .content {
+         padding: 0 0 10px 0 !important;
+         }
+         .content-wrap {
+         padding: 10px !important;
+         }
+         .invoice {
+         width: 100% !important;
+         }
+         .main {
+         border-right: none !important;
+         border-left: none !important;
+         border-radius: 0 !important;
+         }
+         .logo {
+         padding-top: 10px !important;
+         }
+         .footer {
+         margin-top: 10px !important;
+         }
+         .indented {
+         padding-left: 10px;
+         }
+         }
+      </style>
+      <table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6">
+         <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+            <td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center">
+                <img src="{{url}}/bwrs_static/logo-gray.png" alt="" width="250" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" />
+            </td>
+         </tr>
+         <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+            <td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
+               <table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
+                  <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+                     <td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
+                        <table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
+                           <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                              <td class="content-wrap" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 20px; -webkit-text-size-adjust: none;" valign="top">
+                                 <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                       <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
+                                          Verify this email address for your account by clicking the link below.
+                                       </td>
+                                    </tr>
+                                    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                       <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
+                                          <a href="{{url}}/#/verify-email/?userId={{user_id}}&token={{token}}"
+                                             clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                          Verify Email Address Now
+                                          </a>
+                                       </td>
+                                    </tr>
+                                    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                       <td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
+                                          If you did not request to verify your account, you can safely ignore this email.
+                                       </td>
+                                    </tr>
+                                 </table>
+                              </td>
+                           </tr>
+                        </table>
+                        <table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
+                           <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+                              <td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
+                                 <table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
+                                    <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+                                        <td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_static/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
+                                    </tr>
+                                 </table>
+                              </td>
+                           </tr>
+                        </table>
+                     </td>
+                  </tr>
+               </table>
+            </td>
+         </tr>
+      </table>
+   </body>
+</html>

+ 8 - 0
src/static/templates/email/welcome.hbs

@@ -0,0 +1,8 @@
+Welcome
+<!---------------->
+<html>
+<p>
+Thank you for creating an account at <a href="{{url}}">{{url}}</a>. You may now log in with your new account.
+</p>
+<p>If you did not request to create an account, you can safely ignore this email.</p>
+</html>

+ 129 - 0
src/static/templates/email/welcome.html.hbs

@@ -0,0 +1,129 @@
+Welcome
+<!---------------->
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+   <head>
+      <meta name="viewport" content="width=device-width" />
+      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+      <title>Bitwarden_rs</title>
+   </head>
+   <body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6">
+      <style type="text/css">
+          body {
+         margin: 0;
+         font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+         box-sizing: border-box;
+         font-size: 16px;
+         color: #333;
+         line-height: 25px;
+         -webkit-font-smoothing: antialiased;
+         -webkit-text-size-adjust: none;
+         }
+         body * {
+         margin: 0;
+         font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+         box-sizing: border-box;
+         font-size: 16px;
+         color: #333;
+         line-height: 25px;
+         -webkit-font-smoothing: antialiased;
+         -webkit-text-size-adjust: none;
+         }
+         img {
+         max-width: 100%;
+         border: none;
+         }
+         body {
+         -webkit-font-smoothing: antialiased;
+         -webkit-text-size-adjust: none;
+         width: 100% !important;
+         height: 100%;
+         line-height: 25px;
+         }
+         body {
+         background-color: #f6f6f6;
+         }
+         @media only screen and (max-width: 600px) {
+         body {
+         padding: 0 !important;
+         }
+         .container {
+         padding: 0 !important;
+         width: 100% !important;
+         }
+         .container-table {
+         padding: 0 !important;
+         width: 100% !important;
+         }
+         .content {
+         padding: 0 0 10px 0 !important;
+         }
+         .content-wrap {
+         padding: 10px !important;
+         }
+         .invoice {
+         width: 100% !important;
+         }
+         .main {
+         border-right: none !important;
+         border-left: none !important;
+         border-radius: 0 !important;
+         }
+         .logo {
+         padding-top: 10px !important;
+         }
+         .footer {
+         margin-top: 10px !important;
+         }
+         .indented {
+         padding-left: 10px;
+         }
+         }
+      </style>
+      <table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6">
+         <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+            <td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center">
+                <img src="{{url}}/bwrs_static/logo-gray.png" alt="" width="250" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" />
+            </td>
+         </tr>
+         <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+            <td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
+               <table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
+                  <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+                     <td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
+                        <table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
+                           <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                              <td class="content-wrap" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 20px; -webkit-text-size-adjust: none;" valign="top">
+                                 <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                       <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
+                                          Thank you for creating an account at <a href="{{url}}">{{url}}</a>. You may now log in with your new account.
+                                       </td>
+                                    </tr>
+                                    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                       <td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
+                                          If you did not request to create an account, you can safely ignore this email.
+                                       </td>
+                                    </tr>
+                                 </table>
+                              </td>
+                           </tr>
+                        </table>
+                        <table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
+                           <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+                              <td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
+                                 <table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
+                                    <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+                                        <td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_static/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
+                                    </tr>
+                                 </table>
+                              </td>
+                           </tr>
+                        </table>
+                     </td>
+                  </tr>
+               </table>
+            </td>
+         </tr>
+      </table>
+   </body>
+</html>

+ 12 - 0
src/static/templates/email/welcome_must_verify.hbs

@@ -0,0 +1,12 @@
+Welcome
+<!---------------->
+<html>
+<p>
+Thank you for creating an account at <a href="{{url}}">{{url}}</a>. Before you can login with your new account, you must verify this email address by clicking the link below.
+<br>
+<br>
+<a href="{{url}}/#/verify-email/?userId={{user_id}}&token={{token}}">
+Verify Email Address Now</a>
+</p>
+<p>If you did not request to create an account, you can safely ignore this email.</p>
+</html>

+ 137 - 0
src/static/templates/email/welcome_must_verify.html.hbs

@@ -0,0 +1,137 @@
+Welcome
+<!---------------->
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+   <head>
+      <meta name="viewport" content="width=device-width" />
+      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+      <title>Bitwarden_rs</title>
+   </head>
+   <body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6">
+      <style type="text/css">
+          body {
+         margin: 0;
+         font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+         box-sizing: border-box;
+         font-size: 16px;
+         color: #333;
+         line-height: 25px;
+         -webkit-font-smoothing: antialiased;
+         -webkit-text-size-adjust: none;
+         }
+         body * {
+         margin: 0;
+         font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+         box-sizing: border-box;
+         font-size: 16px;
+         color: #333;
+         line-height: 25px;
+         -webkit-font-smoothing: antialiased;
+         -webkit-text-size-adjust: none;
+         }
+         img {
+         max-width: 100%;
+         border: none;
+         }
+         body {
+         -webkit-font-smoothing: antialiased;
+         -webkit-text-size-adjust: none;
+         width: 100% !important;
+         height: 100%;
+         line-height: 25px;
+         }
+         body {
+         background-color: #f6f6f6;
+         }
+         @media only screen and (max-width: 600px) {
+         body {
+         padding: 0 !important;
+         }
+         .container {
+         padding: 0 !important;
+         width: 100% !important;
+         }
+         .container-table {
+         padding: 0 !important;
+         width: 100% !important;
+         }
+         .content {
+         padding: 0 0 10px 0 !important;
+         }
+         .content-wrap {
+         padding: 10px !important;
+         }
+         .invoice {
+         width: 100% !important;
+         }
+         .main {
+         border-right: none !important;
+         border-left: none !important;
+         border-radius: 0 !important;
+         }
+         .logo {
+         padding-top: 10px !important;
+         }
+         .footer {
+         margin-top: 10px !important;
+         }
+         .indented {
+         padding-left: 10px;
+         }
+         }
+      </style>
+      <table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6">
+         <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+            <td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center">
+                <img src="{{url}}/bwrs_static/logo-gray.png" alt="" width="250" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" />
+            </td>
+         </tr>
+         <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+            <td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
+               <table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
+                  <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+                     <td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
+                        <table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
+                           <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                              <td class="content-wrap" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 20px; -webkit-text-size-adjust: none;" valign="top">
+                                 <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                       <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
+                                          Thank you for creating an account at <a href="{{url}}">{{url}}</a>. Before you can login with your new account, you must verify this email address by clicking the link below.
+                                       </td>
+                                    </tr>
+                                    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                       <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
+                                          <a href="{{url}}/#/verify-email/?userId={{user_id}}&token={{token}}"
+                                             clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                          Verify Email Address Now
+                                          </a>
+                                       </td>
+                                    </tr>
+                                    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                       <td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
+                                          If you did not request to create an account, you can safely ignore this email.
+                                       </td>
+                                    </tr>
+                                 </table>
+                              </td>
+                           </tr>
+                        </table>
+                        <table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
+                           <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+                              <td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
+                                 <table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
+                                    <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+                                        <td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_static/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
+                                    </tr>
+                                 </table>
+                              </td>
+                           </tr>
+                        </table>
+                     </td>
+                  </tr>
+               </table>
+            </td>
+         </tr>
+      </table>
+   </body>
+</html>