From 98d12fe143a3f1c8cb033944974da38dbb44ebda Mon Sep 17 00:00:00 2001 From: Mohammad MAsoudie Date: Wed, 25 Jun 2025 15:22:55 +0330 Subject: [PATCH 1/3] refactor email URL generation to use effective mailing domain --- src/mail.rs | 54 ++++++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/mail.rs b/src/mail.rs index b1f37886..4cddc9d6 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -156,7 +156,7 @@ pub async fn send_password_hint(address: &str, hint: Option) -> EmptyRes let (subject, body_html, body_text) = get_text( template_name, json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "hint": hint, }), @@ -172,7 +172,7 @@ pub async fn send_delete_account(address: &str, user_id: &UserId) -> EmptyResult let (subject, body_html, body_text) = get_text( "email/delete_account", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "user_id": user_id, "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), @@ -190,7 +190,7 @@ pub async fn send_verify_email(address: &str, user_id: &UserId) -> EmptyResult { let (subject, body_html, body_text) = get_text( "email/verify_email", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "user_id": user_id, "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), @@ -213,7 +213,7 @@ pub async fn send_register_verify_email(email: &str, token: &str) -> EmptyResult "email/register_verify_email", json!({ // `url.Url` would place the anchor `#` after the query parameters - "url": format!("{}/#/finish-signup/?{query_string}", CONFIG.domain()), + "url": format!("{}/#/finish-signup/?{query_string}", CONFIG.effective_mailing_domain()), "img_src": CONFIG._smtp_img_src(), "email": email, }), @@ -226,7 +226,7 @@ pub async fn send_welcome(address: &str) -> EmptyResult { let (subject, body_html, body_text) = get_text( "email/welcome", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), }), )?; @@ -241,7 +241,7 @@ pub async fn send_welcome_must_verify(address: &str, user_id: &UserId) -> EmptyR let (subject, body_html, body_text) = get_text( "email/welcome_must_verify", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "user_id": user_id, "token": verify_email_token, @@ -255,7 +255,7 @@ pub async fn send_2fa_removed_from_org(address: &str, org_name: &str) -> EmptyRe let (subject, body_html, body_text) = get_text( "email/send_2fa_removed_from_org", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "org_name": org_name, }), @@ -268,7 +268,7 @@ pub async fn send_single_org_removed_from_org(address: &str, org_name: &str) -> let (subject, body_html, body_text) = get_text( "email/send_single_org_removed_from_org", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "org_name": org_name, }), @@ -314,7 +314,7 @@ pub async fn send_invite( "email/send_org_invite", json!({ // `url.Url` would place the anchor `#` after the query parameters - "url": format!("{}/#/accept-organization/?{query_string}", CONFIG.domain()), + "url": format!("{}/#/accept-organization/?{query_string}", CONFIG.effective_mailing_domain()), "img_src": CONFIG._smtp_img_src(), "org_name": org_name, }), @@ -357,7 +357,7 @@ pub async fn send_emergency_access_invite( "email/send_emergency_access_invite", json!({ // `url.Url` would place the anchor `#` after the query parameters - "url": format!("{}/#/accept-emergency/?{query_string}", CONFIG.domain()), + "url": format!("{}/#/accept-emergency/?{query_string}", CONFIG.effective_mailing_domain()), "img_src": CONFIG._smtp_img_src(), "grantor_name": grantor_name, }), @@ -370,7 +370,7 @@ pub async fn send_emergency_access_invite_accepted(address: &str, grantee_email: let (subject, body_html, body_text) = get_text( "email/emergency_access_invite_accepted", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "grantee_email": grantee_email, }), @@ -383,7 +383,7 @@ pub async fn send_emergency_access_invite_confirmed(address: &str, grantor_name: let (subject, body_html, body_text) = get_text( "email/emergency_access_invite_confirmed", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "grantor_name": grantor_name, }), @@ -396,7 +396,7 @@ pub async fn send_emergency_access_recovery_approved(address: &str, grantor_name let (subject, body_html, body_text) = get_text( "email/emergency_access_recovery_approved", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "grantor_name": grantor_name, }), @@ -414,7 +414,7 @@ pub async fn send_emergency_access_recovery_initiated( let (subject, body_html, body_text) = get_text( "email/emergency_access_recovery_initiated", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "grantee_name": grantee_name, "atype": atype, @@ -434,7 +434,7 @@ pub async fn send_emergency_access_recovery_reminder( let (subject, body_html, body_text) = get_text( "email/emergency_access_recovery_reminder", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "grantee_name": grantee_name, "atype": atype, @@ -449,7 +449,7 @@ pub async fn send_emergency_access_recovery_rejected(address: &str, grantor_name let (subject, body_html, body_text) = get_text( "email/emergency_access_recovery_rejected", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "grantor_name": grantor_name, }), @@ -462,7 +462,7 @@ pub async fn send_emergency_access_recovery_timed_out(address: &str, grantee_nam let (subject, body_html, body_text) = get_text( "email/emergency_access_recovery_timed_out", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "grantee_name": grantee_name, "atype": atype, @@ -476,7 +476,7 @@ pub async fn send_invite_accepted(new_user_email: &str, address: &str, org_name: let (subject, body_html, body_text) = get_text( "email/invite_accepted", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "email": new_user_email, "org_name": org_name, @@ -490,7 +490,7 @@ pub async fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult let (subject, body_html, body_text) = get_text( "email/invite_confirmed", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "org_name": org_name, }), @@ -506,7 +506,7 @@ pub async fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTi let (subject, body_html, body_text) = get_text( "email/new_device_logged_in", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "ip": ip, "device_name": upcase_first(&device.name), @@ -531,7 +531,7 @@ pub async fn send_incomplete_2fa_login( let (subject, body_html, body_text) = get_text( "email/incomplete_2fa_login", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "ip": ip, "device_name": upcase_first(device_name), @@ -548,7 +548,7 @@ pub async fn send_token(address: &str, token: &str) -> EmptyResult { let (subject, body_html, body_text) = get_text( "email/twofactor_email", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "token": token, }), @@ -561,7 +561,7 @@ pub async fn send_change_email(address: &str, token: &str) -> EmptyResult { let (subject, body_html, body_text) = get_text( "email/change_email", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "token": token, }), @@ -574,7 +574,7 @@ pub async fn send_change_email_existing(address: &str, acting_address: &str) -> let (subject, body_html, body_text) = get_text( "email/change_email_existing", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "existing_address": address, "acting_address": acting_address, @@ -588,7 +588,7 @@ pub async fn send_test(address: &str) -> EmptyResult { let (subject, body_html, body_text) = get_text( "email/smtp_test", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), }), )?; @@ -600,7 +600,7 @@ pub async fn send_admin_reset_password(address: &str, user_name: &str, org_name: let (subject, body_html, body_text) = get_text( "email/admin_reset_password", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "user_name": user_name, "org_name": org_name, @@ -613,7 +613,7 @@ pub async fn send_protected_action_token(address: &str, token: &str) -> EmptyRes let (subject, body_html, body_text) = get_text( "email/protected_action", json!({ - "url": CONFIG.domain(), + "url": CONFIG.effective_mailing_domain(), "img_src": CONFIG._smtp_img_src(), "token": token, }), From e35de74d8b2a5d39f2a445116a04aaec82c51235 Mon Sep 17 00:00:00 2001 From: Mohammad MAsoudie Date: Wed, 25 Jun 2025 15:23:04 +0330 Subject: [PATCH 2/3] add mailing_domain configuration option and effective_mailing_domain method --- src/config.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/config.rs b/src/config.rs index 5b995c6d..a0e9e4f1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -277,6 +277,7 @@ macro_rules! make_config { "domain_path", "domain", "helo_name", + "mailing_domain", "org_creation_users", "signups_domains_whitelist", "smtp_from", @@ -738,6 +739,8 @@ make_config! { helo_name: String, true, option; /// Embed images as email attachments. smtp_embed_images: bool, true, def, true; + /// Mailing Domain |> Domain used in email templates and links. If not set, uses the main DOMAIN setting. Useful for setting a different public domain for emails while keeping the main domain for internal access. + mailing_domain: String, true, option; /// _smtp_img_src _smtp_img_src: String, false, generated, |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! @@ -1457,6 +1460,11 @@ impl Config { } } } + + /// Get the effective mailing domain - uses mailing_domain if set, otherwise falls back to domain + pub fn effective_mailing_domain(&self) -> String { + self.mailing_domain().unwrap_or_else(|| self.domain()) + } } use handlebars::{ From 74912a04f0cedbe16a63d758ccab6c06f818b481 Mon Sep 17 00:00:00 2001 From: Mohammad MAsoudie Date: Wed, 25 Jun 2025 15:23:11 +0330 Subject: [PATCH 3/3] add mailing domain configuration documentation --- MAILING_DOMAIN.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 MAILING_DOMAIN.md diff --git a/MAILING_DOMAIN.md b/MAILING_DOMAIN.md new file mode 100644 index 00000000..04d8f3b5 --- /dev/null +++ b/MAILING_DOMAIN.md @@ -0,0 +1,54 @@ +# Mailing Domain Configuration + +This feature allows you to configure a separate domain specifically for email templates and links, while keeping your main `DOMAIN` setting for internal server access. + +## Configuration + +### Environment Variable +Set the `MAILING_DOMAIN` environment variable: +```bash +MAILING_DOMAIN=https://public.example.com +``` + +### Docker Environment +Add to your docker-compose.yml or docker run command: +```yaml +environment: + - MAILING_DOMAIN=https://public.example.com +``` + +Or with docker run: +```bash +docker run -e MAILING_DOMAIN=https://public.example.com vaultwarden/server +``` + +### Admin Panel +The mailing domain can also be configured through the admin panel under SMTP Email Settings. + +## Use Cases + +1. **Internal vs Public Access**: Your Vaultwarden server runs on an internal domain (e.g., `http://vaultwarden.internal`) but you want emails to contain links to a public domain (e.g., `https://vault.company.com`). + +2. **Development vs Production**: Use different domains for email links in development and production environments. + +3. **Load Balancer/Proxy**: Your server runs behind a load balancer with a different internal address than the public-facing URL. + +## Behavior + +- If `MAILING_DOMAIN` is set, all email templates will use this domain for links and references +- If `MAILING_DOMAIN` is not set, the system falls back to using the main `DOMAIN` setting +- This affects all email types: invitations, password resets, 2FA emails, notifications, etc. + +## Example + +```bash +# Main domain for server operations +DOMAIN=http://vaultwarden.internal:8080 + +# Public domain for email links +MAILING_DOMAIN=https://vault.company.com +``` + +With this configuration: +- The server operates on `http://vaultwarden.internal:8080` +- All email links will point to `https://vault.company.com` \ No newline at end of file