From 7c216366a76d1e4786e6ce663b67854c829e2577 Mon Sep 17 00:00:00 2001 From: Jared Date: Mon, 9 Feb 2026 14:38:50 -0500 Subject: [PATCH] [PM-31153] email updates for domain claim pt 2 (#6965) * [PM-31361] Enhance domain claimed email notifications * Updated the email template to include the claimed domain name and user email. * Modified the `ClaimedUserDomainClaimedEmails` model to include the domain name. * Adjusted the `SendClaimedDomainUserEmailAsync` method to pass the domain name to the email message. * Added a new test for rendering the domain claimed email to ensure proper content delivery. * Update email templates for domain claimed notifications * Adjusted styles and formatting in the DomainClaimedByOrganization email template for improved readability. * Modified the TitleContactUs layout to ensure proper rendering of titles. * Updated the HandlebarsMailService to include HTML line breaks in the email title for better presentation. * Update TitleContactUs email template to center-align title text for improved presentation * Refine TitleContactUs email template by removing unnecessary text-align property for improved consistency in styling * Fix PR comments * Update test/Core.Test/Platform/Mail/DomainClaimedEmailRenderTest.cs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * Update test/Core.Test/Platform/Mail/DomainClaimedEmailRenderTest.cs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * Update test/Core.Test/Platform/Mail/DomainClaimedEmailRenderTest.cs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * Remove unnecessary comments --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- .../VerifyOrganizationDomainCommand.cs | 2 +- .../DomainClaimedByOrganization.html.hbs | 21 +- .../DomainClaimedByOrganization.text.hbs | 2 + .../Layouts/TitleContactUs.html.hbs | 4 +- .../ClaimedUserDomainClaimedEmails.cs | 2 +- .../ClaimedDomainUserNotificationViewModel.cs | 3 + .../Platform/Mail/HandlebarsMailService.cs | 11 +- .../VerifyOrganizationDomainCommandTests.cs | 3 +- .../Mail/DomainClaimedEmailRenderTest.cs | 195 ++++++++++++++++++ 9 files changed, 226 insertions(+), 17 deletions(-) create mode 100644 test/Core.Test/Platform/Mail/DomainClaimedEmailRenderTest.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs index e6cc3da2a2..aec6380ce2 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs @@ -157,6 +157,6 @@ public class VerifyOrganizationDomainCommand( var organization = await organizationRepository.GetByIdAsync(domain.OrganizationId); - await mailService.SendClaimedDomainUserEmailAsync(new ClaimedUserDomainClaimedEmails(domainUserEmails, organization)); + await mailService.SendClaimedDomainUserEmailAsync(new ClaimedUserDomainClaimedEmails(domainUserEmails, organization, domain.DomainName)); } } diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.html.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.html.hbs index 766aefa804..18a4f93ac5 100644 --- a/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.html.hbs +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.html.hbs @@ -1,18 +1,23 @@ {{#>TitleContactUsHtmlLayout}} - + + + -
- What this means for you -
    -
  • Your day-to-day use of Bitwarden remains the same.
  • -
  • Only store work-related items in your {{OrganizationName}} vault.
  • -
  • {{OrganizationName}} admins now manage your account, meaning they can revoke or delete your account.
  • +
+

An {{OrganizationName}} admin has claimed the domain @{{{DomainName}}}. Your email address {{{UserEmail}}} matches this, so your Bitwarden account is now managed by {{OrganizationName}}.

+
+

What this means for you

+
    +
  • Your day-to-day use of Bitwarden remains the same.
  • +
  • Only store work-related items in your {{OrganizationName}} vault.
  • +
  • {{OrganizationName}} admins now manage your account, meaning they can revoke or delete your account.
- For more information, please refer to the following help article: Claimed accounts + +

For more information, please refer to the following help article: Claimed accounts

diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.text.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.text.hbs index e33b8fe1b9..4a87d30c34 100644 --- a/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.text.hbs +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.text.hbs @@ -1,3 +1,5 @@ +An {{OrganizationName}} admin has claimed the domain @{{{DomainName}}}. Your email address {{{UserEmail}}} matches this, so your Bitwarden account is now managed by {{OrganizationName}}. + What this means for you: - Your day-to-day use of Bitwarden remains the same. - Only store work-related items in your {{OrganizationName}} vault. diff --git a/src/Core/MailTemplates/Handlebars/Layouts/TitleContactUs.html.hbs b/src/Core/MailTemplates/Handlebars/Layouts/TitleContactUs.html.hbs index ed0d7cd9af..1fa6c014f0 100644 --- a/src/Core/MailTemplates/Handlebars/Layouts/TitleContactUs.html.hbs +++ b/src/Core/MailTemplates/Handlebars/Layouts/TitleContactUs.html.hbs @@ -1,11 +1,11 @@ -{{#>FullUpdatedHtmlLayout}} +{{#>FullUpdatedHtmlLayout}}
- {{TitleFirst}}{{TitleSecondBold}}{{TitleThird}} + {{{TitleFirst}}}{{TitleSecondBold}}{{TitleThird}}
diff --git a/src/Core/Models/Data/Organizations/ClaimedUserDomainClaimedEmails.cs b/src/Core/Models/Data/Organizations/ClaimedUserDomainClaimedEmails.cs index 2b73fc1525..f44274c6a5 100644 --- a/src/Core/Models/Data/Organizations/ClaimedUserDomainClaimedEmails.cs +++ b/src/Core/Models/Data/Organizations/ClaimedUserDomainClaimedEmails.cs @@ -2,4 +2,4 @@ namespace Bit.Core.Models.Data.Organizations; -public record ClaimedUserDomainClaimedEmails(IEnumerable EmailList, Organization Organization); +public record ClaimedUserDomainClaimedEmails(IEnumerable EmailList, Organization Organization, string DomainName); diff --git a/src/Core/Models/Mail/ClaimedDomainUserNotificationViewModel.cs b/src/Core/Models/Mail/ClaimedDomainUserNotificationViewModel.cs index fa1ed5ab45..deb5571e96 100644 --- a/src/Core/Models/Mail/ClaimedDomainUserNotificationViewModel.cs +++ b/src/Core/Models/Mail/ClaimedDomainUserNotificationViewModel.cs @@ -6,4 +6,7 @@ namespace Bit.Core.Models.Mail; public class ClaimedDomainUserNotificationViewModel : BaseTitleContactUsMailModel { public string OrganizationName { get; init; } + public string DomainName { get; init; } + public string EmailDomain { get; init; } + public string UserEmail { get; init; } } diff --git a/src/Core/Platform/Mail/HandlebarsMailService.cs b/src/Core/Platform/Mail/HandlebarsMailService.cs index 179e887a9e..298e335c9f 100644 --- a/src/Core/Platform/Mail/HandlebarsMailService.cs +++ b/src/Core/Platform/Mail/HandlebarsMailService.cs @@ -632,16 +632,19 @@ public class HandlebarsMailService : IMailService public async Task SendClaimedDomainUserEmailAsync(ClaimedUserDomainClaimedEmails emailList) { await EnqueueMailAsync(emailList.EmailList.Select(email => - CreateMessage(email, emailList.Organization))); + CreateMessage(email, emailList.Organization, emailList.DomainName))); return; - MailQueueMessage CreateMessage(string emailAddress, Organization org) => + MailQueueMessage CreateMessage(string emailAddress, Organization org, string domainName) => new(CreateDefaultMessage($"Important update to your Bitwarden account", emailAddress), "AdminConsole.DomainClaimedByOrganization", new ClaimedDomainUserNotificationViewModel { - TitleFirst = $"Important update to your Bitwarden account", - OrganizationName = CoreHelpers.SanitizeForEmail(org.DisplayName(), false) + TitleFirst = $"Important update to your
Bitwarden account", + OrganizationName = CoreHelpers.SanitizeForEmail(org.DisplayName(), false), + DomainName = domainName, + EmailDomain = emailAddress.Split('@').LastOrDefault() ?? "", + UserEmail = emailAddress }); } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs index ef4c2c941e..730489a9fc 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs @@ -282,6 +282,7 @@ public class VerifyOrganizationDomainCommandTests await sutProvider.GetDependency().Received().SendClaimedDomainUserEmailAsync( Arg.Is(x => x.EmailList.Count(e => e.EndsWith(domain.DomainName)) == mockedUsers.Count && - x.Organization.Id == organization.Id)); + x.Organization.Id == organization.Id && + x.DomainName == domain.DomainName)); } } diff --git a/test/Core.Test/Platform/Mail/DomainClaimedEmailRenderTest.cs b/test/Core.Test/Platform/Mail/DomainClaimedEmailRenderTest.cs new file mode 100644 index 0000000000..57e5f43d8a --- /dev/null +++ b/test/Core.Test/Platform/Mail/DomainClaimedEmailRenderTest.cs @@ -0,0 +1,195 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Platform.Mail.Delivery; +using Bit.Core.Platform.Mail.Enqueuing; +using Bit.Core.Services.Mail; +using Bit.Core.Settings; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Platform.Mail; + +public class DomainClaimedEmailRenderTest +{ + [Fact] + public async Task RenderDomainClaimedEmail_ToVerifyTemplate() + { + var globalSettings = new GlobalSettings + { + Mail = new GlobalSettings.MailSettings + { + ReplyToEmail = "no-reply@bitwarden.com", + Smtp = new GlobalSettings.MailSettings.SmtpSettings + { + Host = "localhost", + Port = 1025, + StartTls = false, + Ssl = false + } + }, + SiteName = "Bitwarden" + }; + + var mailDeliveryService = Substitute.For(); + var mailEnqueuingService = new BlockingMailEnqueuingService(); + var distributedCache = Substitute.For(); + var logger = Substitute.For>(); + + var mailService = new HandlebarsMailService( + globalSettings, + mailDeliveryService, + mailEnqueuingService, + distributedCache, + logger + ); + + var organization = new Organization + { + Id = Guid.NewGuid(), + Name = "Acme Corporation" + }; + + var testEmails = new List + { + "alice@acme.com", + "bob@acme.com", + "charlie@acme.com" + }; + + var emailList = new ClaimedUserDomainClaimedEmails( + testEmails, + organization, + "acme.com" + ); + + await mailService.SendClaimedDomainUserEmailAsync(emailList); + + await mailDeliveryService.Received(3).SendEmailAsync(Arg.Any()); + + var calls = mailDeliveryService.ReceivedCalls() + .Where(call => call.GetMethodInfo().Name == "SendEmailAsync") + .ToList(); + + Assert.Equal(3, calls.Count); + + foreach (var call in calls) + { + var mailMessage = call.GetArguments()[0] as Bit.Core.Models.Mail.MailMessage; + Assert.NotNull(mailMessage); + + var recipient = mailMessage.ToEmails.First(); + + Assert.Contains("@acme.com", mailMessage.HtmlContent); + Assert.Contains(recipient, mailMessage.HtmlContent); + Assert.DoesNotContain("[at]", mailMessage.HtmlContent); + Assert.DoesNotContain("[dot]", mailMessage.HtmlContent); + } + } + + [Fact(Skip = "For local development - requires MailCatcher at localhost:10250")] + public async Task SendDomainClaimedEmail_ToMailCatcher() + { + var globalSettings = new GlobalSettings + { + Mail = new GlobalSettings.MailSettings + { + ReplyToEmail = "no-reply@bitwarden.com", + Smtp = new GlobalSettings.MailSettings.SmtpSettings + { + Host = "localhost", + Port = 10250, + StartTls = false, + Ssl = false + } + }, + SiteName = "Bitwarden" + }; + + var mailDeliveryLogger = Substitute.For>(); + var mailDeliveryService = new MailKitSmtpMailDeliveryService(globalSettings, mailDeliveryLogger); + var mailEnqueuingService = new BlockingMailEnqueuingService(); + var distributedCache = Substitute.For(); + var logger = Substitute.For>(); + + var mailService = new HandlebarsMailService( + globalSettings, + mailDeliveryService, + mailEnqueuingService, + distributedCache, + logger + ); + + var organization = new Organization + { + Id = Guid.NewGuid(), + Name = "Acme Corporation" + }; + + var testEmails = new List + { + "alice@acme.com", + "bob@acme.com" + }; + + var emailList = new ClaimedUserDomainClaimedEmails( + testEmails, + organization, + "acme.com" + ); + + await mailService.SendClaimedDomainUserEmailAsync(emailList); + } + + [Fact(Skip = "This test sends actual emails and is for manual template verification only")] + public async Task RenderDomainClaimedEmail_WithSpecialCharacters() + { + var globalSettings = new GlobalSettings + { + Mail = new GlobalSettings.MailSettings + { + Smtp = new GlobalSettings.MailSettings.SmtpSettings + { + Host = "localhost", + Port = 1025, + StartTls = false, + Ssl = false + } + }, + SiteName = "Bitwarden" + }; + + var mailDeliveryService = Substitute.For(); + var mailEnqueuingService = new BlockingMailEnqueuingService(); + var distributedCache = Substitute.For(); + var logger = Substitute.For>(); + + var mailService = new HandlebarsMailService( + globalSettings, + mailDeliveryService, + mailEnqueuingService, + distributedCache, + logger + ); + + var organization = new Organization + { + Id = Guid.NewGuid(), + Name = "Test Corp & Co." + }; + + var testEmails = new List + { + "test.user+tag@example.com" + }; + + var emailList = new ClaimedUserDomainClaimedEmails( + testEmails, + organization, + "example.com" + ); + + await mailService.SendClaimedDomainUserEmailAsync(emailList); + } +}