diff --git a/src/Core/MailTemplates/Handlebars/SecurityTasksNotificationV2.html.hbs b/src/Core/MailTemplates/Handlebars/SecurityTasksNotificationV2.html.hbs
new file mode 100644
index 000000000000..157815c16028
--- /dev/null
+++ b/src/Core/MailTemplates/Handlebars/SecurityTasksNotificationV2.html.hbs
@@ -0,0 +1,33 @@
+{{#>SecurityTasksHtmlLayout}}
+
+
+ |
+ Keep you and your organization safe by changing passwords that are weak, reused, or exposed in a data breach.
+ |
+
+
+ |
+ Launch the Bitwarden extension to review your at-risk passwords.
+ |
+
+
+
+
+
+ |
+ {{formatAdminOwnerEmails AdminOwnerEmails}}
+ |
+
+
+{{/SecurityTasksHtmlLayout}}
diff --git a/src/Core/MailTemplates/Handlebars/SecurityTasksNotificationV2.text.hbs b/src/Core/MailTemplates/Handlebars/SecurityTasksNotificationV2.text.hbs
new file mode 100644
index 000000000000..cdcab8cf415f
--- /dev/null
+++ b/src/Core/MailTemplates/Handlebars/SecurityTasksNotificationV2.text.hbs
@@ -0,0 +1,18 @@
+{{#>SecurityTasksHtmlLayout}}
+Keep you and your organization safe by changing passwords that are weak, reused, or exposed in a data breach.
+
+Launch the Bitwarden extension to review your at-risk passwords.
+
+Review at-risk passwords ({{{ReviewPasswordsUrl}}})
+
+{{#if AdminOwnerEmails.[0]}}
+ {{#if AdminOwnerEmails.[1]}}
+ This request was initiated by
+ {{#each AdminOwnerEmails}}
+ {{#if @last}}and {{/if}}{{this}}{{#unless @last}}, {{/unless}}
+ {{/each}}.
+ {{else}}
+ This request was initiated by {{AdminOwnerEmails.[0]}}.
+ {{/if}}
+{{/if}}
+{{/SecurityTasksHtmlLayout}}
diff --git a/src/Core/Platform/Mail/HandlebarsMailService.cs b/src/Core/Platform/Mail/HandlebarsMailService.cs
index 903379fa74a2..c933ff52a813 100644
--- a/src/Core/Platform/Mail/HandlebarsMailService.cs
+++ b/src/Core/Platform/Mail/HandlebarsMailService.cs
@@ -1617,8 +1617,10 @@ public async Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable<
await _mailDeliveryService.SendEmailAsync(message);
}
- public async Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable securityTaskNotifications, IEnumerable adminOwnerEmails)
+ public async Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable securityTaskNotifications, IEnumerable adminOwnerEmails, bool useV2Template = false)
{
+ var templateName = useV2Template ? "SecurityTasksNotificationV2" : "SecurityTasksNotification";
+
MailQueueMessage CreateMessage(UserSecurityTasksCount notification)
{
var sanitizedOrgName = CoreHelpers.SanitizeForEmail(org.DisplayName(), false);
@@ -1631,7 +1633,7 @@ MailQueueMessage CreateMessage(UserSecurityTasksCount notification)
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
};
message.Category = "SecurityTasksNotification";
- return new MailQueueMessage(message, "SecurityTasksNotification", model);
+ return new MailQueueMessage(message, templateName, model);
}
var messageModels = securityTaskNotifications.Select(CreateMessage);
await EnqueueMailAsync(messageModels.ToList());
diff --git a/src/Core/Platform/Mail/IMailService.cs b/src/Core/Platform/Mail/IMailService.cs
index 6c91943e59fe..48a70b05a0c3 100644
--- a/src/Core/Platform/Mail/IMailService.cs
+++ b/src/Core/Platform/Mail/IMailService.cs
@@ -144,5 +144,5 @@ Task SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(string email, string
#nullable enable
Task SendClaimedDomainUserEmailAsync(ClaimedUserDomainClaimedEmails emailList);
Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable adminEmails, Guid organizationId, string email, string? userName);
- Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable securityTaskNotifications, IEnumerable adminOwnerEmails);
+ Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable securityTaskNotifications, IEnumerable adminOwnerEmails, bool useV2Template = false);
}
diff --git a/src/Core/Platform/Mail/NoopMailService.cs b/src/Core/Platform/Mail/NoopMailService.cs
index ef8b5f9c78f8..408c0cc08a82 100644
--- a/src/Core/Platform/Mail/NoopMailService.cs
+++ b/src/Core/Platform/Mail/NoopMailService.cs
@@ -354,7 +354,7 @@ public Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable securityTaskNotifications, IEnumerable adminOwnerEmails)
+ public Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable securityTaskNotifications, IEnumerable adminOwnerEmails, bool useV2Template = false)
{
return Task.FromResult(0);
}
diff --git a/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs b/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs
index fbd957afae88..ef11b1d5f895 100644
--- a/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs
+++ b/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs
@@ -1,6 +1,7 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
+using Bit.Core;
using Bit.Core.Enums;
using Bit.Core.NotificationCenter.Commands.Interfaces;
using Bit.Core.NotificationCenter.Entities;
@@ -21,6 +22,7 @@ public class CreateManyTaskNotificationsCommand : ICreateManyTaskNotificationsCo
private readonly ICreateNotificationCommand _createNotificationCommand;
private readonly IPushNotificationService _pushNotificationService;
private readonly IOrganizationUserRepository _organizationUserRepository;
+ private readonly IFeatureService _featureService;
public CreateManyTaskNotificationsCommand(
IGetSecurityTasksNotificationDetailsQuery getSecurityTasksNotificationDetailsQuery,
@@ -28,7 +30,8 @@ public CreateManyTaskNotificationsCommand(
IMailService mailService,
ICreateNotificationCommand createNotificationCommand,
IPushNotificationService pushNotificationService,
- IOrganizationUserRepository organizationUserRepository)
+ IOrganizationUserRepository organizationUserRepository,
+ IFeatureService featureService)
{
_getSecurityTasksNotificationDetailsQuery = getSecurityTasksNotificationDetailsQuery;
_organizationRepository = organizationRepository;
@@ -36,6 +39,7 @@ public CreateManyTaskNotificationsCommand(
_createNotificationCommand = createNotificationCommand;
_pushNotificationService = pushNotificationService;
_organizationUserRepository = organizationUserRepository;
+ _featureService = featureService;
}
public async Task CreateAsync(Guid orgId, IEnumerable securityTasks)
@@ -62,7 +66,9 @@ public async Task CreateAsync(Guid orgId, IEnumerable securityTask
// Ensure proper deserialization of emails
var orgAdminAndOwnerEmails = orgAdminEmails.Concat(orgOwnerEmails).Distinct().ToList();
- await _mailService.SendBulkSecurityTaskNotificationsAsync(organization, userTaskCount, orgAdminAndOwnerEmails);
+ var useV2Template = _featureService.IsEnabled(FeatureFlagKeys.VFO1Foundation);
+
+ await _mailService.SendBulkSecurityTaskNotificationsAsync(organization, userTaskCount, orgAdminAndOwnerEmails, useV2Template);
// Break securityTaskCiphers into separate lists by user Id
var securityTaskCiphersByUser = securityTaskCiphers.GroupBy(x => x.UserId)
diff --git a/test/Core.Test/Vault/Commands/CreateManyTaskNotificationsCommandTest.cs b/test/Core.Test/Vault/Commands/CreateManyTaskNotificationsCommandTest.cs
new file mode 100644
index 000000000000..b8d40408391b
--- /dev/null
+++ b/test/Core.Test/Vault/Commands/CreateManyTaskNotificationsCommandTest.cs
@@ -0,0 +1,75 @@
+using Bit.Core.AdminConsole.Entities;
+using Bit.Core.Repositories;
+using Bit.Core.Services;
+using Bit.Core.Vault.Entities;
+using Bit.Core.Vault.Models.Data;
+using Bit.Core.Vault.Queries;
+using Bit.Test.Common.AutoFixture;
+using Bit.Test.Common.AutoFixture.Attributes;
+using NSubstitute;
+using Xunit;
+
+namespace Bit.Core.Test.Vault.Commands;
+
+[SutProviderCustomize]
+public class CreateManyTaskNotificationsCommandTest
+{
+ [Theory]
+ [BitAutoData]
+ public async Task CreateAsync_VFO1FoundationDisabled_UsesV1Template(
+ SutProvider sutProvider,
+ Organization organization,
+ ICollection securityTasks,
+ UserSecurityTaskCipher userSecurityTaskCipher)
+ {
+ userSecurityTaskCipher.UserId = Guid.NewGuid();
+ Setup(sutProvider, organization, userSecurityTaskCipher, vfo1FoundationEnabled: false);
+
+ await sutProvider.Sut.CreateAsync(organization.Id, securityTasks);
+
+ await sutProvider.GetDependency().Received(1)
+ .SendBulkSecurityTaskNotificationsAsync(
+ organization,
+ Arg.Any>(),
+ Arg.Any>(),
+ false);
+ }
+
+ [Theory]
+ [BitAutoData]
+ public async Task CreateAsync_VFO1FoundationEnabled_UsesV2Template(
+ SutProvider sutProvider,
+ Organization organization,
+ ICollection securityTasks,
+ UserSecurityTaskCipher userSecurityTaskCipher)
+ {
+ userSecurityTaskCipher.UserId = Guid.NewGuid();
+ Setup(sutProvider, organization, userSecurityTaskCipher, vfo1FoundationEnabled: true);
+
+ await sutProvider.Sut.CreateAsync(organization.Id, securityTasks);
+
+ await sutProvider.GetDependency().Received(1)
+ .SendBulkSecurityTaskNotificationsAsync(
+ organization,
+ Arg.Any>(),
+ Arg.Any>(),
+ true);
+ }
+
+ private static void Setup(
+ SutProvider sutProvider,
+ Organization organization,
+ UserSecurityTaskCipher userSecurityTaskCipher,
+ bool vfo1FoundationEnabled)
+ {
+ sutProvider.GetDependency()
+ .IsEnabled(FeatureFlagKeys.VFO1Foundation)
+ .Returns(vfo1FoundationEnabled);
+ sutProvider.GetDependency()
+ .GetNotificationDetailsByManyIds(organization.Id, Arg.Any>())
+ .Returns(new List { userSecurityTaskCipher });
+ sutProvider.GetDependency()
+ .GetByIdAsync(organization.Id)
+ .Returns(organization);
+ }
+}