From f41ba2a60f161dde69f0a42ad9cf5d896a64e874 Mon Sep 17 00:00:00 2001 From: BlackDex Date: Mon, 17 Oct 2022 17:23:21 +0200 Subject: [PATCH 1/5] Fix master password hint update not working. - The Master Password Hint input has changed it's location to the password update form. This PR updates the the code to process this. - Also changed the `ProfileData` struct to exclude `Culture` and `MasterPasswordHint`, since both are not used at all, and when not defined they will also not be allocated. Fixes #2833 --- src/api/core/accounts.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index a980271b..a43ca4b0 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -193,9 +193,8 @@ async fn profile(headers: Headers, conn: DbConn) -> Json { #[derive(Deserialize, Debug)] #[allow(non_snake_case)] struct ProfileData { - #[serde(rename = "Culture")] - _Culture: String, // Ignored, always use en-US - MasterPasswordHint: Option, + // Culture: String, // Ignored, always use en-US + // MasterPasswordHint: Option, // Ignored, has been moved to ChangePassData Name: String, } @@ -216,8 +215,6 @@ async fn post_profile(data: JsonUpcase, headers: Headers, conn: DbC let mut user = headers.user; user.name = data.Name; - user.password_hint = clean_password_hint(&data.MasterPasswordHint); - enforce_password_hint_setting(&user.password_hint)?; user.save(&conn).await?; Ok(Json(user.to_json(&conn).await)) @@ -260,6 +257,7 @@ async fn post_keys(data: JsonUpcase, headers: Headers, conn: DbConn) - struct ChangePassData { MasterPasswordHash: String, NewMasterPasswordHash: String, + MasterPasswordHint: Option, Key: String, } @@ -272,6 +270,9 @@ async fn post_password(data: JsonUpcase, headers: Headers, conn: err!("Invalid password") } + user.password_hint = clean_password_hint(&data.MasterPasswordHint); + enforce_password_hint_setting(&user.password_hint)?; + user.set_password( &data.NewMasterPasswordHash, Some(vec![String::from("post_rotatekey"), String::from("get_contacts"), String::from("get_public_keys")]), From 6576914e5541b49181c3a7bfd987b5c15fe81a58 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Tue, 27 Sep 2022 23:19:35 +0200 Subject: [PATCH 2/5] fix invitations of new users when mail is disabled If you add a new user that has already been Invited to another organization they will be Accepted automatically. This should not be possible because they cannot be Confirmed until they have completed their registration. It is also not necessary because their invitation will be accepted automatically once they register. --- src/api/core/organizations.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index dca4f393..9f2178e7 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -600,11 +600,7 @@ async fn send_invite(org_id: String, data: JsonUpcase, headers: Admi for email in data.Emails.iter() { let email = email.to_lowercase(); - let mut user_org_status = if CONFIG.mail_enabled() { - UserOrgStatus::Invited as i32 - } else { - UserOrgStatus::Accepted as i32 // Automatically mark user as accepted if no email invites - }; + let mut user_org_status = UserOrgStatus::Invited as i32; let user = match User::find_by_mail(&email, &conn).await { None => { if !CONFIG.invitations_allowed() { @@ -622,13 +618,16 @@ async fn send_invite(org_id: String, data: JsonUpcase, headers: Admi let mut user = User::new(email.clone()); user.save(&conn).await?; - user_org_status = UserOrgStatus::Invited as i32; user } Some(user) => { if UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &conn).await.is_some() { err!(format!("User already in organization: {}", email)) } else { + // automatically accept existing users if mail is disabled + if !CONFIG.mail_enabled() && !user.password_hash.is_empty() { + user_org_status = UserOrgStatus::Accepted as i32; + } user } } From 4d1b860dada9bf56d71aa8beea87419fb0e087d3 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Fri, 30 Sep 2022 19:14:26 +0200 Subject: [PATCH 3/5] attach images to email Set SMTP_EMBED_IMAGES option to false if you don't want to attach images to the mail. NOTE: If you have customized the template files `email_header.hbs` and `email_footer.hbs` you can replace `{url}/vw_static/` to `{img_url}` to support both URL schemes --- .env.template | 3 ++ src/config.rs | 12 +++++ src/mail.rs | 55 +++++++++++++++++++-- src/static/templates/email/email_footer.hbs | 2 +- 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) -> 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::>()[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 @@ - +
GitHubGitHub
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 @@ From 0e6f6e612ab22c975ea9cbedaac17c403e691ee7 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Thu, 6 Oct 2022 11:59:47 +0200 Subject: [PATCH 4/5] use static_files() for email attachments Apply suggestions from code review Co-authored-by: Mathijs van Veluw --- src/api/mod.rs | 1 + src/api/web.rs | 2 +- src/config.rs | 2 +- src/mail.rs | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index b9e9f38c..7bff978b 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -22,6 +22,7 @@ pub use crate::api::{ notifications::{start_notification_server, Notify, UpdateType}, web::catchers as web_catchers, web::routes as web_routes, + web::static_files, }; use crate::util; diff --git a/src/api/web.rs b/src/api/web.rs index 2ad94db8..cfc4b9e0 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -89,7 +89,7 @@ fn alive(_conn: DbConn) -> Json { } #[get("/vw_static/")] -fn static_files(filename: String) -> Result<(ContentType, &'static [u8]), Error> { +pub fn static_files(filename: String) -> Result<(ContentType, &'static [u8]), Error> { match filename.as_ref() { "mail-github.png" => Ok((ContentType::PNG, include_bytes!("../static/images/mail-github.png"))), "logo-gray.png" => Ok((ContentType::PNG, include_bytes!("../static/images/logo-gray.png"))), diff --git a/src/config.rs b/src/config.rs index 4cac70eb..936f15df 100644 --- a/src/config.rs +++ b/src/config.rs @@ -767,7 +767,7 @@ fn generate_smtp_img_src(embed_images: bool, domain: &str) -> String { if embed_images { "cid:".to_string() } else { - format!("{}/vw_static/", domain) + format!("{domain}/vw_static/") } } diff --git a/src/mail.rs b/src/mail.rs index e613da6f..fce76e17 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -496,11 +496,11 @@ 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() { + let logo_gray_body = Body::new(crate::api::static_files("logo-gray.png".to_string()).unwrap().1.to_vec()); + let mail_github_body = Body::new(crate::api::static_files("mail-github.png".to_string()).unwrap().1.to_vec()); MultiPart::alternative().singlepart(SinglePart::plain(body_text)).multipart( MultiPart::related() .singlepart(SinglePart::html(body_html)) From 23f1f8a5768c1f361cd83f0f4035b329848d5e3a Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Thu, 6 Oct 2022 00:18:20 +0200 Subject: [PATCH 5/5] allow registration without invite link if signups are allowed invited users should be able to complete their registration even when they don't have the invite link at hand. --- src/api/core/accounts.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index a43ca4b0..df8bfccf 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -101,11 +101,7 @@ async fn register(data: JsonUpcase, conn: DbConn) -> JsonResult { let mut user = match User::find_by_mail(&email, &conn).await { Some(user) => { if !user.password_hash.is_empty() { - if CONFIG.is_signup_allowed(&email) { - err!("User already exists") - } else { - err!("Registration not allowed or user already exists") - } + err!("Registration not allowed or user already exists") } if let Some(token) = data.Token { @@ -121,10 +117,10 @@ async fn register(data: JsonUpcase, conn: DbConn) -> JsonResult { user_org.save(&conn).await?; } user - } else if EmergencyAccess::find_invited_by_grantee_email(&email, &conn).await.is_some() { + } else if CONFIG.is_signup_allowed(&email) + || EmergencyAccess::find_invited_by_grantee_email(&email, &conn).await.is_some() + { user - } else if CONFIG.is_signup_allowed(&email) { - err!("Account with this email already exists") } else { err!("Registration not allowed or user already exists") }