diff options
-rw-r--r-- | .env.template | 3 | ||||
-rw-r--r-- | src/config.rs | 12 | ||||
-rw-r--r-- | src/mail.rs | 55 | ||||
-rw-r--r-- | src/static/templates/email/email_footer.hbs | 2 | ||||
-rw-r--r-- | src/static/templates/email/email_header.hbs | 2 |
5 files changed, 69 insertions, 5 deletions
diff --git a/.env.template b/.env.template index 60b5b73b..1e5ff101 100644 --- a/.env.template +++ b/.env.template @@ -367,6 +367,9 @@ ## but might need to be changed in case it trips some anti-spam filters # HELO_NAME= +## Embed images as email attachments +# SMTP_EMBED_IMAGES=false + ## SMTP debugging ## When set to true this will output very detailed SMTP messages. ## WARNING: This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting! diff --git a/src/config.rs b/src/config.rs index 3a2cf958..4cac70eb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -602,6 +602,10 @@ make_config! { smtp_timeout: u64, true, def, 15; /// Server name sent during HELO |> By default this value should be is on the machine's hostname, but might need to be changed in case it trips some anti-spam filters helo_name: String, true, option; + /// Embed images as email attachments. + smtp_embed_images: bool, true, def, true; + /// Internal + _smtp_img_src: String, false, gen, |c| generate_smtp_img_src(c.smtp_embed_images, &c.domain); /// Enable SMTP debugging (Know the risks!) |> DANGEROUS: Enabling this will output very detailed SMTP messages. This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting! smtp_debug: bool, false, def, false; /// Accept Invalid Certs (Know the risks!) |> DANGEROUS: Allow invalid certificates. This option introduces significant vulnerabilities to man-in-the-middle attacks! @@ -759,6 +763,14 @@ fn extract_url_path(url: &str) -> String { } } +fn generate_smtp_img_src(embed_images: bool, domain: &str) -> String { + if embed_images { + "cid:".to_string() + } else { + format!("{}/vw_static/", domain) + } +} + /// Generate the correct URL for the icon service. /// This will be used within icons.rs to call the external icon service. fn generate_icon_service_url(icon_service: &str) -> String { diff --git a/src/mail.rs b/src/mail.rs index 5cc12658..e613da6f 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -4,7 +4,7 @@ use chrono::NaiveDateTime; use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; use lettre::{ - message::{Mailbox, Message, MultiPart}, + message::{Attachment, Body, Mailbox, Message, MultiPart, SinglePart}, transport::smtp::authentication::{Credentials, Mechanism as SmtpAuthMechanism}, transport::smtp::client::{Tls, TlsParameters}, transport::smtp::extension::ClientId, @@ -117,7 +117,14 @@ pub async fn send_password_hint(address: &str, hint: Option<String>) -> EmptyRes "email/pw_hint_none" }; - let (subject, body_html, body_text) = get_text(template_name, json!({ "hint": hint, "url": CONFIG.domain() }))?; + let (subject, body_html, body_text) = get_text( + template_name, + json!({ + "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), + "hint": hint, + }), + )?; send_email(address, &subject, body_html, body_text).await } @@ -130,6 +137,7 @@ pub async fn send_delete_account(address: &str, uuid: &str) -> EmptyResult { "email/delete_account", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "user_id": uuid, "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), "token": delete_token, @@ -147,6 +155,7 @@ pub async fn send_verify_email(address: &str, uuid: &str) -> EmptyResult { "email/verify_email", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "user_id": uuid, "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), "token": verify_email_token, @@ -161,6 +170,7 @@ pub async fn send_welcome(address: &str) -> EmptyResult { "email/welcome", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), }), )?; @@ -175,6 +185,7 @@ pub async fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult "email/welcome_must_verify", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "user_id": uuid, "token": verify_email_token, }), @@ -188,6 +199,7 @@ pub async fn send_2fa_removed_from_org(address: &str, org_name: &str) -> EmptyRe "email/send_2fa_removed_from_org", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "org_name": org_name, }), )?; @@ -200,6 +212,7 @@ pub async fn send_single_org_removed_from_org(address: &str, org_name: &str) -> "email/send_single_org_removed_from_org", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "org_name": org_name, }), )?; @@ -228,6 +241,7 @@ pub async fn send_invite( "email/send_org_invite", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "org_id": org_id.as_deref().unwrap_or("_"), "org_user_id": org_user_id.as_deref().unwrap_or("_"), "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), @@ -260,6 +274,7 @@ pub async fn send_emergency_access_invite( "email/send_emergency_access_invite", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "emer_id": emer_id.unwrap_or_else(|| "_".to_string()), "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), "grantor_name": grantor_name, @@ -275,6 +290,7 @@ pub async fn send_emergency_access_invite_accepted(address: &str, grantee_email: "email/emergency_access_invite_accepted", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "grantee_email": grantee_email, }), )?; @@ -287,6 +303,7 @@ pub async fn send_emergency_access_invite_confirmed(address: &str, grantor_name: "email/emergency_access_invite_confirmed", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "grantor_name": grantor_name, }), )?; @@ -299,6 +316,7 @@ pub async fn send_emergency_access_recovery_approved(address: &str, grantor_name "email/emergency_access_recovery_approved", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "grantor_name": grantor_name, }), )?; @@ -316,6 +334,7 @@ pub async fn send_emergency_access_recovery_initiated( "email/emergency_access_recovery_initiated", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "grantee_name": grantee_name, "atype": atype, "wait_time_days": wait_time_days, @@ -335,6 +354,7 @@ pub async fn send_emergency_access_recovery_reminder( "email/emergency_access_recovery_reminder", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "grantee_name": grantee_name, "atype": atype, "days_left": days_left, @@ -349,6 +369,7 @@ pub async fn send_emergency_access_recovery_rejected(address: &str, grantor_name "email/emergency_access_recovery_rejected", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "grantor_name": grantor_name, }), )?; @@ -361,6 +382,7 @@ pub async fn send_emergency_access_recovery_timed_out(address: &str, grantee_nam "email/emergency_access_recovery_timed_out", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "grantee_name": grantee_name, "atype": atype, }), @@ -374,6 +396,7 @@ pub async fn send_invite_accepted(new_user_email: &str, address: &str, org_name: "email/invite_accepted", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "email": new_user_email, "org_name": org_name, }), @@ -387,6 +410,7 @@ pub async fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult "email/invite_confirmed", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "org_name": org_name, }), )?; @@ -403,6 +427,7 @@ pub async fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTi "email/new_device_logged_in", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "ip": ip, "device": device, "datetime": crate::util::format_naive_datetime_local(dt, fmt), @@ -421,6 +446,7 @@ pub async fn send_incomplete_2fa_login(address: &str, ip: &str, dt: &NaiveDateTi "email/incomplete_2fa_login", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "ip": ip, "device": device, "datetime": crate::util::format_naive_datetime_local(dt, fmt), @@ -436,6 +462,7 @@ pub async fn send_token(address: &str, token: &str) -> EmptyResult { "email/twofactor_email", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "token": token, }), )?; @@ -448,6 +475,7 @@ pub async fn send_change_email(address: &str, token: &str) -> EmptyResult { "email/change_email", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), "token": token, }), )?; @@ -460,6 +488,7 @@ pub async fn send_test(address: &str) -> EmptyResult { "email/smtp_test", json!({ "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), }), )?; @@ -467,13 +496,33 @@ pub async fn send_test(address: &str) -> EmptyResult { } async fn send_email(address: &str, subject: &str, body_html: String, body_text: String) -> EmptyResult { + let logo_gray_body = Body::new(include_bytes!("static/images/logo-gray.png").to_vec()); + let mail_github_body = Body::new(include_bytes!("static/images/mail-github.png").to_vec()); let smtp_from = &CONFIG.smtp_from(); + + let body = if CONFIG.smtp_embed_images() { + MultiPart::alternative().singlepart(SinglePart::plain(body_text)).multipart( + MultiPart::related() + .singlepart(SinglePart::html(body_html)) + .singlepart( + Attachment::new_inline(String::from("logo-gray.png")) + .body(logo_gray_body, "image/png".parse().unwrap()), + ) + .singlepart( + Attachment::new_inline(String::from("mail-github.png")) + .body(mail_github_body, "image/png".parse().unwrap()), + ), + ) + } else { + MultiPart::alternative_plain_html(body_text, body_html) + }; + let email = Message::builder() .message_id(Some(format!("<{}@{}>", crate::util::get_uuid(), smtp_from.split('@').collect::<Vec<&str>>()[1]))) .to(Mailbox::new(None, Address::from_str(address)?)) .from(Mailbox::new(Some(CONFIG.smtp_from_name()), Address::from_str(smtp_from)?)) .subject(subject) - .multipart(MultiPart::alternative_plain_html(body_text, body_html))?; + .multipart(body)?; match mailer().send(email).await { Ok(_) => Ok(()), diff --git a/src/static/templates/email/email_footer.hbs b/src/static/templates/email/email_footer.hbs index 33177317..7bf30682 100644 --- a/src/static/templates/email/email_footer.hbs +++ b/src/static/templates/email/email_footer.hbs @@ -10,7 +10,7 @@ <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/vaultwarden" 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}}/vw_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> + <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/vaultwarden" 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="{{img_src}}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> diff --git a/src/static/templates/email/email_header.hbs b/src/static/templates/email/email_header.hbs index a1e7cc27..811f997d 100644 --- a/src/static/templates/email/email_header.hbs +++ b/src/static/templates/email/email_header.hbs @@ -81,7 +81,7 @@ <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}}/vw_static/logo-gray.png" alt="" width="190" 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%;" /> + <img src="{{img_src}}logo-gray.png" alt="Vaultwarden" width="190" 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;"> |