mirror of
https://github.com/bitwarden/server
synced 2026-02-12 06:23:28 +00:00
[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>
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
{{#>TitleContactUsHtmlLayout}}
|
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="display: table; width:100%; padding: 30px; text-align: left;" align="center">
|
||||
<tr>
|
||||
<td style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 16px; line-height: 24px; margin-top: 30px; margin-bottom: 25px; margin-left: 35px; margin-right: 35px;">
|
||||
<b>What this means for you</b>
|
||||
<ul>
|
||||
<li>Your day-to-day use of Bitwarden remains the same.</li>
|
||||
<li>Only store work-related items in your {{OrganizationName}} vault.</li>
|
||||
<li>{{OrganizationName}} admins now manage your account, meaning they can revoke or delete your account.</li>
|
||||
<td style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 14px; line-height: 24px; margin-top: 30px; margin-bottom: 25px; margin-left: 35px; margin-right: 35px;">
|
||||
<p style="margin: 0 0 24px 0;">An {{OrganizationName}} admin has claimed the domain @{{{DomainName}}}. Your email address {{{UserEmail}}} matches this, so your Bitwarden account is now managed by {{OrganizationName}}.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 14px; line-height: 24px; margin-top: 30px; margin-bottom: 25px; margin-left: 35px; margin-right: 35px;">
|
||||
<p style="margin: 0 0 12px 0;"><b>What this means for you</b></p>
|
||||
<ul style="margin: 0 0 24px 0; padding-left: 20px;">
|
||||
<li style="margin-bottom: 8px;">Your day-to-day use of Bitwarden remains the same.</li>
|
||||
<li style="margin-bottom: 8px;">Only store work-related items in your {{OrganizationName}} vault.</li>
|
||||
<li style="margin-bottom: 8px;">{{OrganizationName}} admins now manage your account, meaning they can revoke or delete your account.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 16px; line-height: 24px; margin-top: 30px; margin-bottom: 25px; margin-left: 35px; margin-right: 35px;">
|
||||
For more information, please refer to the following help article: <a href="https://bitwarden.com/help/claimed-accounts">Claimed accounts</a>
|
||||
<td style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 14px; line-height: 24px; margin-top: 30px; margin-bottom: 25px; margin-left: 35px; margin-right: 35px;">
|
||||
<p style="margin: 0;">For more information, please refer to the following help article: <a href="https://bitwarden.com/help/claimed-accounts" style="color: #175DDC; font-weight: 700; text-decoration: none;">Claimed accounts</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{{#>FullUpdatedHtmlLayout}}
|
||||
{{#>FullUpdatedHtmlLayout}}
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #175DDC;padding-top:45px; ">
|
||||
<tr>
|
||||
<td align="center" valign="top" width="70%" class="templateColumnContainer">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="padding-left:30px; padding-right: 5px; padding-bottom: 35px;">
|
||||
<tr>
|
||||
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 24px; color: #ffffff; line-height: 32px; font-weight: 400; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
{{TitleFirst}}<b class="white-title">{{TitleSecondBold}}</b>{{TitleThird}}
|
||||
{{{TitleFirst}}}<b class="white-title">{{TitleSecondBold}}</b>{{TitleThird}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace Bit.Core.Models.Data.Organizations;
|
||||
|
||||
public record ClaimedUserDomainClaimedEmails(IEnumerable<string> EmailList, Organization Organization);
|
||||
public record ClaimedUserDomainClaimedEmails(IEnumerable<string> EmailList, Organization Organization, string DomainName);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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<br>Bitwarden account",
|
||||
OrganizationName = CoreHelpers.SanitizeForEmail(org.DisplayName(), false),
|
||||
DomainName = domainName,
|
||||
EmailDomain = emailAddress.Split('@').LastOrDefault() ?? "",
|
||||
UserEmail = emailAddress
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -282,6 +282,7 @@ public class VerifyOrganizationDomainCommandTests
|
||||
await sutProvider.GetDependency<IMailService>().Received().SendClaimedDomainUserEmailAsync(
|
||||
Arg.Is<ClaimedUserDomainClaimedEmails>(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));
|
||||
}
|
||||
}
|
||||
|
||||
195
test/Core.Test/Platform/Mail/DomainClaimedEmailRenderTest.cs
Normal file
195
test/Core.Test/Platform/Mail/DomainClaimedEmailRenderTest.cs
Normal file
@@ -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<IMailDeliveryService>();
|
||||
var mailEnqueuingService = new BlockingMailEnqueuingService();
|
||||
var distributedCache = Substitute.For<IDistributedCache>();
|
||||
var logger = Substitute.For<ILogger<HandlebarsMailService>>();
|
||||
|
||||
var mailService = new HandlebarsMailService(
|
||||
globalSettings,
|
||||
mailDeliveryService,
|
||||
mailEnqueuingService,
|
||||
distributedCache,
|
||||
logger
|
||||
);
|
||||
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Acme Corporation"
|
||||
};
|
||||
|
||||
var testEmails = new List<string>
|
||||
{
|
||||
"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<Bit.Core.Models.Mail.MailMessage>());
|
||||
|
||||
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<ILogger<MailKitSmtpMailDeliveryService>>();
|
||||
var mailDeliveryService = new MailKitSmtpMailDeliveryService(globalSettings, mailDeliveryLogger);
|
||||
var mailEnqueuingService = new BlockingMailEnqueuingService();
|
||||
var distributedCache = Substitute.For<IDistributedCache>();
|
||||
var logger = Substitute.For<ILogger<HandlebarsMailService>>();
|
||||
|
||||
var mailService = new HandlebarsMailService(
|
||||
globalSettings,
|
||||
mailDeliveryService,
|
||||
mailEnqueuingService,
|
||||
distributedCache,
|
||||
logger
|
||||
);
|
||||
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Acme Corporation"
|
||||
};
|
||||
|
||||
var testEmails = new List<string>
|
||||
{
|
||||
"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<IMailDeliveryService>();
|
||||
var mailEnqueuingService = new BlockingMailEnqueuingService();
|
||||
var distributedCache = Substitute.For<IDistributedCache>();
|
||||
var logger = Substitute.For<ILogger<HandlebarsMailService>>();
|
||||
|
||||
var mailService = new HandlebarsMailService(
|
||||
globalSettings,
|
||||
mailDeliveryService,
|
||||
mailEnqueuingService,
|
||||
distributedCache,
|
||||
logger
|
||||
);
|
||||
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Test Corp & Co."
|
||||
};
|
||||
|
||||
var testEmails = new List<string>
|
||||
{
|
||||
"test.user+tag@example.com"
|
||||
};
|
||||
|
||||
var emailList = new ClaimedUserDomainClaimedEmails(
|
||||
testEmails,
|
||||
organization,
|
||||
"example.com"
|
||||
);
|
||||
|
||||
await mailService.SendClaimedDomainUserEmailAsync(emailList);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user