From d1a5c4de46d5fb4a39ce3c838e3d386bce444c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:42:11 +0000 Subject: [PATCH] [PM-32796] Fix bulk reinvite timeouts by moving updated org emails from IMailer to IMailService (#7105) --- ...maticallyConfirmOrganizationUserCommand.cs | 2 +- .../ConfirmOrganizationUserCommand.cs | 3 +- .../BulkResendOrganizationInvitesCommand.cs | 2 +- .../ResendOrganizationInviteCommand.cs | 8 +- .../SendOrganizationInvitesCommand.cs | 258 +---- .../ISendOrganizationConfirmationCommand.cs | 8 - .../SendOrganizationConfirmationCommand.cs | 107 +- ...onConfirmationEnterpriseTeamsView.html.hbs | 794 ++++++++++++++ ...onConfirmationEnterpriseTeamsView.text.hbs | 4 + ...izationConfirmationFamilyFreeView.html.hbs | 969 +++++++++++++++++ ...izationConfirmationFamilyFreeView.text.hbs | 4 + ...teEnterpriseTeamsExistingUserView.html.hbs | 976 ++++++++++++++++++ ...teEnterpriseTeamsExistingUserView.text.hbs | 19 + ...nInviteEnterpriseTeamsNewUserView.html.hbs | 908 ++++++++++++++++ ...nInviteEnterpriseTeamsNewUserView.text.hbs | 15 + ...ionInviteFamiliesExistingUserView.html.hbs | 908 ++++++++++++++++ ...ionInviteFamiliesExistingUserView.text.hbs | 15 + ...nizationInviteFamiliesNewUserView.html.hbs | 908 ++++++++++++++++ ...nizationInviteFamiliesNewUserView.text.hbs | 15 + .../OrganizationInviteFreeView.html.hbs | 908 ++++++++++++++++ .../OrganizationInviteFreeView.text.hbs | 15 + .../Models/Mail/OrganizationInvitesInfo.cs | 5 +- .../Platform/Mail/HandlebarsMailService.cs | 157 +++ src/Core/Platform/Mail/IMailService.cs | 2 + src/Core/Platform/Mail/NoopMailService.cs | 10 + .../AutomaticallyConfirmUsersCommandTests.cs | 16 +- .../ConfirmOrganizationUserCommandTests.cs | 10 +- .../SendOrganizationInvitesCommandTests.cs | 233 +---- ...endOrganizationConfirmationCommandTests.cs | 212 +--- 29 files changed, 6699 insertions(+), 792 deletions(-) create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.text.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.text.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.text.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.text.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.text.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesNewUserView.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesNewUserView.text.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFreeView.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFreeView.text.hbs diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs index 37d57b5ad9..3cb12bb2ff 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs @@ -191,7 +191,7 @@ public class AutomaticallyConfirmOrganizationUserCommand(IOrganizationUserReposi } else { - await mailService.SendOrganizationConfirmedEmailAsync(organization.Name, userEmail, accessSecretsManager); + await mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), userEmail, accessSecretsManager); } } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs index 3c7e51229d..007f28a2f3 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs @@ -51,7 +51,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand IPolicyRequirementQuery policyRequirementQuery, IFeatureService featureService, ICollectionRepository collectionRepository, - IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator, ISendOrganizationConfirmationCommand sendOrganizationConfirmationCommand) + IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator, + ISendOrganizationConfirmationCommand sendOrganizationConfirmationCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/BulkResendOrganizationInvitesCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/BulkResendOrganizationInvitesCommand.cs index c7c80bd937..cf51987f95 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/BulkResendOrganizationInvitesCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/BulkResendOrganizationInvitesCommand.cs @@ -59,7 +59,7 @@ public class BulkResendOrganizationInvitesCommand : IBulkResendOrganizationInvit if (validUsers.Any()) { await _sendOrganizationInvitesCommand.SendInvitesAsync( - new SendInvitesRequest(validUsers, org)); + new SendInvitesRequest(validUsers, org, invitingUserId: invitingUserId)); result.AddRange(validUsers.Select(u => Tuple.Create(u, ""))); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/ResendOrganizationInviteCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/ResendOrganizationInviteCommand.cs index 7e68af7816..297a10b939 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/ResendOrganizationInviteCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/ResendOrganizationInviteCommand.cs @@ -45,12 +45,14 @@ public class ResendOrganizationInviteCommand : IResendOrganizationInviteCommand { throw new BadRequestException("Organization invalid."); } - await SendInviteAsync(organizationUser, organization, initOrganization); + await SendInviteAsync(organizationUser, organization, initOrganization, invitingUserId); } - private async Task SendInviteAsync(OrganizationUser organizationUser, Organization organization, bool initOrganization) => + private async Task SendInviteAsync(OrganizationUser organizationUser, Organization organization, + bool initOrganization, Guid? invitingUserId) => await _sendOrganizationInvitesCommand.SendInvitesAsync(new SendInvitesRequest( users: [organizationUser], organization: organization, - initOrganization: initOrganization)); + initOrganization: initOrganization, + invitingUserId: invitingUserId)); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs index 4dc379d804..30fdb7c85a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs @@ -1,23 +1,17 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable -using System.Net; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationInvite; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.Auth.Models.Business; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; -using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Models.Mail; -using Bit.Core.Platform.Mail.Mailer; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Settings; using Bit.Core.Tokens; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; @@ -29,42 +23,27 @@ public class SendOrganizationInvitesCommand( IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory, IDataProtectorTokenFactory dataProtectorTokenFactory, IMailService mailService, - IMailer mailer, - IFeatureService featureService, - GlobalSettings globalSettings) : ISendOrganizationInvitesCommand + IFeatureService featureService) : ISendOrganizationInvitesCommand { - // New user (no existing account) email constants - private const string _newUserSubject = "set up a Bitwarden account for you"; - private const string _newUserTitle = "set up a Bitwarden password manager account for you."; - private const string _newUserButton = "Finish account setup"; - - // Existing user email constants - private const string _existingUserSubject = "invited you to their Bitwarden organization"; - private const string _existingUserTitle = "invited you to join them on Bitwarden"; - private const string _existingUserButton = "Accept invitation"; - - // Free organization email constants - private const string _freeOrgNewUserSubject = "You have been invited to Bitwarden Password Manager"; - private const string _freeOrgExistingUserSubject = "You have been invited to a Bitwarden Organization"; - private const string _freeOrgTitle = "You have been invited to Bitwarden Password Manager"; - public async Task SendInvitesAsync(SendInvitesRequest request) { - var orgInvitesInfo = await BuildOrganizationInvitesInfoAsync(request.Users, request.Organization, request.InitOrganization); - if (featureService.IsEnabled(FeatureFlagKeys.UpdateJoinOrganizationEmailTemplate)) { var inviterEmail = await GetInviterEmailAsync(request.InvitingUserId); - await SendNewInviteEmailsAsync(orgInvitesInfo, inviterEmail); + var orgInvitesInfo = await BuildOrganizationInvitesInfoAsync( + request.Users, request.Organization, request.InitOrganization, inviterEmail); + await mailService.SendUpdatedOrganizationInviteEmailsAsync(orgInvitesInfo); } else { + var orgInvitesInfo = await BuildOrganizationInvitesInfoAsync( + request.Users, request.Organization, request.InitOrganization); await mailService.SendOrganizationInviteEmailsAsync(orgInvitesInfo); } } private async Task BuildOrganizationInvitesInfoAsync(IEnumerable orgUsers, - Organization organization, bool initOrganization = false) + Organization organization, bool initOrganization = false, string inviterEmail = null) { // Materialize the sequence into a list to avoid multiple enumeration warnings var orgUsersList = orgUsers.ToList(); @@ -109,227 +88,11 @@ public class SendOrganizationInvitesCommand( orgSsoLoginRequiredPolicyEnabled, orgUsersWithExpTokens, orgUserHasExistingUserDict, - initOrganization + initOrganization, + inviterEmail ); } - private async Task SendNewInviteEmailsAsync(OrganizationInvitesInfo orgInvitesInfo, string inviterEmail) - { - foreach (var (orgUser, token) in orgInvitesInfo.OrgUserTokenPairs) - { - var userHasExistingUser = orgInvitesInfo.OrgUserHasExistingUserDict[orgUser.Id]; - - await SendInviteEmailAsync( - userHasExistingUser, - orgInvitesInfo, - orgUser, - token, - inviterEmail - ); - } - } - - private async Task SendInviteEmailAsync( - bool userHasExistingUser, - OrganizationInvitesInfo orgInvitesInfo, - OrganizationUser orgUser, - ExpiringToken token, - string inviterEmail) - { - if (PlanConstants.EnterprisePlanTypes.Contains(orgInvitesInfo.PlanType) || - PlanConstants.TeamsPlanTypes.Contains(orgInvitesInfo.PlanType) || - orgInvitesInfo.PlanType == PlanType.TeamsStarter || - orgInvitesInfo.PlanType == PlanType.TeamsStarter2023 || - orgInvitesInfo.PlanType == PlanType.Custom) - { - if (userHasExistingUser) - { - await SendEnterpriseTeamsExistingUserInviteAsync(orgInvitesInfo, orgUser, token, inviterEmail); - } - else - { - await SendEnterpriseTeamsNewUserInviteAsync(orgInvitesInfo, orgUser, token, inviterEmail); - } - } - else if (PlanConstants.FamiliesPlanTypes.Contains(orgInvitesInfo.PlanType)) - { - if (userHasExistingUser) - { - await SendFamiliesExistingUserInviteAsync(orgInvitesInfo, orgUser, token, inviterEmail); - } - else - { - await SendFamiliesNewUserInviteAsync(orgInvitesInfo, orgUser, token, inviterEmail); - } - } - else - { - // Free plan (default) - await SendFreeOrganizationInviteAsync(orgInvitesInfo, orgUser, token, inviterEmail, userHasExistingUser); - } - } - - private async Task SendEnterpriseTeamsNewUserInviteAsync( - OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail) - { - var organizationName = WebUtility.HtmlDecode(orgInvitesInfo.OrganizationName); - var mail = new OrganizationInviteEnterpriseTeamsNewUser - { - ToEmails = [orgUser.Email], - Subject = $"{organizationName} {_newUserSubject}", - View = CreateEnterpriseTeamsNewUserView(orgInvitesInfo, orgUser, token, inviterEmail) - }; - await mailer.SendEmail(mail); - } - - private async Task SendEnterpriseTeamsExistingUserInviteAsync( - OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail) - { - var organizationName = WebUtility.HtmlDecode(orgInvitesInfo.OrganizationName); - var mail = new OrganizationInviteEnterpriseTeamsExistingUser - { - ToEmails = [orgUser.Email], - Subject = $"{organizationName} {_existingUserSubject}", - View = CreateEnterpriseTeamsExistingUserView(orgInvitesInfo, orgUser, token, inviterEmail) - }; - await mailer.SendEmail(mail); - } - - private async Task SendFamiliesNewUserInviteAsync( - OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail) - { - var organizationName = WebUtility.HtmlDecode(orgInvitesInfo.OrganizationName); - var mail = new OrganizationInviteFamiliesNewUser - { - ToEmails = [orgUser.Email], - Subject = $"{organizationName} {_newUserSubject}", - View = CreateFamiliesNewUserView(orgInvitesInfo, orgUser, token, inviterEmail) - }; - await mailer.SendEmail(mail); - } - - private async Task SendFamiliesExistingUserInviteAsync( - OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail) - { - var organizationName = WebUtility.HtmlDecode(orgInvitesInfo.OrganizationName); - var mail = new OrganizationInviteFamiliesExistingUser - { - ToEmails = [orgUser.Email], - Subject = $"{organizationName} {_existingUserSubject}", - View = CreateFamiliesExistingUserView(orgInvitesInfo, orgUser, token, inviterEmail) - }; - await mailer.SendEmail(mail); - } - - private async Task SendFreeOrganizationInviteAsync( - OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail, bool userHasExistingUser) - { - var mail = new OrganizationInviteFree - { - ToEmails = [orgUser.Email], - Subject = userHasExistingUser ? _freeOrgExistingUserSubject : _freeOrgNewUserSubject, - View = CreateFreeView(orgInvitesInfo, orgUser, token, inviterEmail) - }; - await mailer.SendEmail(mail); - } - - private OrganizationInviteEnterpriseTeamsNewUserView CreateEnterpriseTeamsNewUserView( - OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail) - { - var organizationName = WebUtility.HtmlDecode(orgInvitesInfo.OrganizationName); - return new OrganizationInviteEnterpriseTeamsNewUserView - { - OrganizationName = organizationName, - Email = orgUser.Email, - ExpirationDate = FormatExpirationDate(token.ExpirationDate), - Url = BuildInvitationUrl(orgInvitesInfo, orgUser, token), - ButtonText = _newUserButton, - InviterEmail = inviterEmail - }; - } - - private OrganizationInviteEnterpriseTeamsExistingUserView CreateEnterpriseTeamsExistingUserView( - OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail) - { - var organizationName = WebUtility.HtmlDecode(orgInvitesInfo.OrganizationName); - return new OrganizationInviteEnterpriseTeamsExistingUserView - { - OrganizationName = organizationName, - Email = orgUser.Email, - ExpirationDate = FormatExpirationDate(token.ExpirationDate), - Url = BuildInvitationUrl(orgInvitesInfo, orgUser, token), - ButtonText = _existingUserButton, - InviterEmail = inviterEmail - }; - } - - private OrganizationInviteFamiliesNewUserView CreateFamiliesNewUserView( - OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail) - { - var organizationName = WebUtility.HtmlDecode(orgInvitesInfo.OrganizationName); - return new OrganizationInviteFamiliesNewUserView - { - OrganizationName = organizationName, - Email = orgUser.Email, - ExpirationDate = FormatExpirationDate(token.ExpirationDate), - Url = BuildInvitationUrl(orgInvitesInfo, orgUser, token), - ButtonText = _newUserButton, - InviterEmail = inviterEmail - }; - } - - private OrganizationInviteFamiliesExistingUserView CreateFamiliesExistingUserView( - OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail) - { - var organizationName = WebUtility.HtmlDecode(orgInvitesInfo.OrganizationName); - return new OrganizationInviteFamiliesExistingUserView - { - OrganizationName = organizationName, - Email = orgUser.Email, - ExpirationDate = FormatExpirationDate(token.ExpirationDate), - Url = BuildInvitationUrl(orgInvitesInfo, orgUser, token), - ButtonText = _existingUserButton, - InviterEmail = inviterEmail - }; - } - - private OrganizationInviteFreeView CreateFreeView( - OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail) - { - var organizationName = WebUtility.HtmlDecode(orgInvitesInfo.OrganizationName); - return new OrganizationInviteFreeView - { - OrganizationName = organizationName, - Email = orgUser.Email, - ExpirationDate = FormatExpirationDate(token.ExpirationDate), - Url = BuildInvitationUrl(orgInvitesInfo, orgUser, token), - ButtonText = _existingUserButton, - InviterEmail = inviterEmail - }; - } - - private string BuildInvitationUrl(OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token) - { - var baseUrl = $"{globalSettings.BaseServiceUri.VaultWithHash}/accept-organization"; - var queryParams = new List - { - $"organizationId={orgUser.OrganizationId}", - $"organizationUserId={orgUser.Id}", - $"email={WebUtility.UrlEncode(orgUser.Email)}", - $"organizationName={WebUtility.UrlEncode(orgInvitesInfo.OrganizationName)}", - $"token={WebUtility.UrlEncode(token.Token)}", - $"initOrganization={orgInvitesInfo.InitOrganization}", - $"orgUserHasExistingUser={orgInvitesInfo.OrgUserHasExistingUserDict[orgUser.Id]}" - }; - - if (orgInvitesInfo.OrgSsoEnabled && orgInvitesInfo.OrgSsoLoginRequiredPolicyEnabled) - { - queryParams.Add($"orgSsoIdentifier={orgInvitesInfo.OrgSsoIdentifier}"); - } - - return $"{baseUrl}?{string.Join("&", queryParams)}"; - } - private async Task GetInviterEmailAsync(Guid? invitingUserId) { if (!invitingUserId.HasValue) @@ -340,7 +103,4 @@ public class SendOrganizationInvitesCommand( var invitingUser = await userRepository.GetByIdAsync(invitingUserId.Value); return invitingUser?.Email; } - - private static string FormatExpirationDate(DateTime expirationDate) => - $"{expirationDate.ToLongDateString()} {expirationDate.ToShortTimeString()} UTC"; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/ISendOrganizationConfirmationCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/ISendOrganizationConfirmationCommand.cs index ae4d7acda5..d11f5d9b7e 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/ISendOrganizationConfirmationCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/ISendOrganizationConfirmationCommand.cs @@ -11,12 +11,4 @@ public interface ISendOrganizationConfirmationCommand /// The email address of the user to send the confirmation to. /// Whether the user has access to Secrets Manager. Task SendConfirmationAsync(Organization organization, string userEmail, bool accessSecretsManager); - - /// - /// Sends organization confirmation emails to multiple users. - /// - /// The organization to send the confirmation emails for. - /// The email addresses of the users to send confirmations to. - /// Whether the users have access to Secrets Manager. - Task SendConfirmationsAsync(Organization organization, IEnumerable userEmails, bool accessSecretsManager); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/SendOrganizationConfirmationCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/SendOrganizationConfirmationCommand.cs index 952478ce36..93861cf5c4 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/SendOrganizationConfirmationCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/SendOrganizationConfirmationCommand.cs @@ -1,110 +1,13 @@ -using System.Net; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationConfirmation; -using Bit.Core.Billing.Enums; -using Bit.Core.Platform.Mail.Mailer; -using Bit.Core.Settings; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Services; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation; -public class SendOrganizationConfirmationCommand(IMailer mailer, GlobalSettings globalSettings) : ISendOrganizationConfirmationCommand +public class SendOrganizationConfirmationCommand(IMailService mailService) + : ISendOrganizationConfirmationCommand { - private const string _titleFirst = "You're confirmed as a member of "; - private const string _titleThird = "!"; - - private static string GetConfirmationSubject(string organizationName) => - $"You can now access items from {organizationName}"; - private string GetWebVaultUrl(bool accessSecretsManager) => accessSecretsManager - ? globalSettings.BaseServiceUri.VaultWithHashAndSecretManagerProduct - : globalSettings.BaseServiceUri.VaultWithHash; - public async Task SendConfirmationAsync(Organization organization, string userEmail, bool accessSecretsManager = false) { - await SendConfirmationsAsync(organization, [userEmail], accessSecretsManager); - } - - public async Task SendConfirmationsAsync(Organization organization, IEnumerable userEmails, bool accessSecretsManager = false) - { - var userEmailsList = userEmails.ToList(); - - if (userEmailsList.Count == 0) - { - return; - } - - var organizationName = WebUtility.HtmlDecode(organization.Name); - - if (IsEnterpriseOrTeamsPlan(organization.PlanType)) - { - await SendEnterpriseTeamsEmailsAsync(userEmailsList, organizationName, accessSecretsManager); - return; - } - - await SendFamilyFreeConfirmEmailsAsync(userEmailsList, organizationName, accessSecretsManager); - } - - private async Task SendEnterpriseTeamsEmailsAsync(List userEmailsList, string organizationName, bool accessSecretsManager) - { - var mail = new OrganizationConfirmationEnterpriseTeams - { - ToEmails = userEmailsList, - Subject = GetConfirmationSubject(organizationName), - View = new OrganizationConfirmationEnterpriseTeamsView - { - OrganizationName = organizationName, - TitleFirst = _titleFirst, - TitleSecondBold = organizationName, - TitleThird = _titleThird, - WebVaultUrl = GetWebVaultUrl(accessSecretsManager) - } - }; - - await mailer.SendEmail(mail); - } - - private async Task SendFamilyFreeConfirmEmailsAsync(List userEmailsList, string organizationName, bool accessSecretsManager) - { - var mail = new OrganizationConfirmationFamilyFree - { - ToEmails = userEmailsList, - Subject = GetConfirmationSubject(organizationName), - View = new OrganizationConfirmationFamilyFreeView - { - OrganizationName = organizationName, - TitleFirst = _titleFirst, - TitleSecondBold = organizationName, - TitleThird = _titleThird, - WebVaultUrl = GetWebVaultUrl(accessSecretsManager) - } - }; - - await mailer.SendEmail(mail); - } - - - private static bool IsEnterpriseOrTeamsPlan(PlanType planType) - { - return planType switch - { - PlanType.TeamsMonthly2019 or - PlanType.TeamsAnnually2019 or - PlanType.TeamsMonthly2020 or - PlanType.TeamsAnnually2020 or - PlanType.TeamsMonthly2023 or - PlanType.TeamsAnnually2023 or - PlanType.TeamsStarter2023 or - PlanType.TeamsMonthly or - PlanType.TeamsAnnually or - PlanType.TeamsStarter or - PlanType.EnterpriseMonthly2019 or - PlanType.EnterpriseAnnually2019 or - PlanType.EnterpriseMonthly2020 or - PlanType.EnterpriseAnnually2020 or - PlanType.EnterpriseMonthly2023 or - PlanType.EnterpriseAnnually2023 or - PlanType.EnterpriseMonthly or - PlanType.EnterpriseAnnually => true, - _ => false - }; + await mailService.SendUpdatedOrganizationConfirmedEmailAsync(organization, userEmail, accessSecretsManager); } } diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.html.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.html.hbs new file mode 100644 index 0000000000..67788eeb4c --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.html.hbs @@ -0,0 +1,794 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +

+ You can now share passwords with members of {{OrganizationName}}! +

+ +
+ + + + + + + +
+ + Log in + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +
+ + +
+ +
+ + + + + +
+ + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
As a member of {{ OrganizationName }}:
+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Organization Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
Your account is owned by {{OrganizationName}} and is subject to their security and management policies.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Share Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + + + + + +
+ +
You can easily access and share passwords with your team.
+ +
+ + + +
+ +
+ + +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +

+ Learn more about Bitwarden +

+

+ Find user guides, product documentation, and videos on the + Bitwarden Help Center. +

+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + +
+ +

+ © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, + Santa Barbara, CA, USA +

+

+ Always confirm you are on a trusted Bitwarden domain before logging + in:
+ bitwarden.com + | + Learn why we include this +

+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.text.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.text.hbs new file mode 100644 index 0000000000..38c45f2dd1 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.text.hbs @@ -0,0 +1,4 @@ +{{#>TitleContactUsTextLayout}} + You may now access logins and other items {{OrganizationName}} has shared with you from your Bitwarden vault. + Tip: Use the Bitwarden mobile app to quickly save logins and auto-fill forms. Download from the App Store or Google Play. +{{/TitleContactUsTextLayout}} diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.html.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.html.hbs new file mode 100644 index 0000000000..3e5e2554ed --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.html.hbs @@ -0,0 +1,969 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +

+ You can now share passwords with members of {{OrganizationName}}! +

+ +
+ + + + + + + +
+ + Log in + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +
+ + +
+ +
+ + + + + +
+ + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
As a member of {{ OrganizationName }}:
+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Group Users Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
You can access passwords {{OrganizationName}} has shared with you.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Share Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + + + + + +
+ +
You can easily share passwords with friends, family, or coworkers.
+ +
+ + + +
+ +
+ + +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+ +
Download Bitwarden on all devices
+ +
+ +
Already using the + browser extension? Download the Bitwarden mobile app from the + App Store + or + Google Play + to quickly save logins and autofill forms on the go.
+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + + + Download on the App Store + + + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ + + + + + + +
+ + + + Get it on Google Play + + + +
+ +
+ +
+ + +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +

+ Learn more about Bitwarden +

+

+ Find user guides, product documentation, and videos on the + Bitwarden Help Center. +

+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + +
+ +

+ © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, + Santa Barbara, CA, USA +

+

+ Always confirm you are on a trusted Bitwarden domain before logging + in:
+ bitwarden.com + | + Learn why we include this +

+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.text.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.text.hbs new file mode 100644 index 0000000000..38c45f2dd1 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.text.hbs @@ -0,0 +1,4 @@ +{{#>TitleContactUsTextLayout}} + You may now access logins and other items {{OrganizationName}} has shared with you from your Bitwarden vault. + Tip: Use the Bitwarden mobile app to quickly save logins and auto-fill forms. Download from the App Store or Google Play. +{{/TitleContactUsTextLayout}} diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.html.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.html.hbs new file mode 100644 index 0000000000..72a0e667c0 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.html.hbs @@ -0,0 +1,976 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +

+ {{OrganizationName}} invited you to join them on Bitwarden +

+ +
+ + + + + + + +
+ + {{ButtonText}} + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +
+ + +
+ +
+ + + + + +
+ + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
{{OrganizationName}} is rolling out Bitwarden to increase security and protect your sensitive data. Once you accept this invitation, you can:
+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Store Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Store logins securely so you never forget your passwords.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Autofill Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Sign in to accounts quickly by filling passwords with one click.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Share Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Share logins easily with your team.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
{{#if InviterEmail}} + This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}} + {{else}} + This invitation expires {{ExpirationDate}} + {{/if}}
+ +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+ +
Your existing account will be owned by {{OrganizationName}}
+ +
+ +
By accepting this invitation, your account ({{Email}}) will be owned by {{OrganizationName}} and will be subject to their security and management policies. Contact your administrator with any questions or concerns.
+ +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +

+ Learn more about Bitwarden +

+

+ Find user guides, product documentation, and videos on the + Bitwarden Help Center. +

+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + +
+ +

+ © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa + Barbara, CA, USA +

+

+ Always confirm you are on a trusted Bitwarden domain before logging + in:
+ bitwarden.com | + Learn why we include this +

+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.text.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.text.hbs new file mode 100644 index 0000000000..18a7bfe759 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.text.hbs @@ -0,0 +1,19 @@ +{{#>TitleContactUsTextLayout}} +{{OrganizationName}} is rolling out Bitwarden to increase security and protect your sensitive data. Once you accept this invitation, you can: + +- Store logins securely so you never forget your passwords. +- Sign in to accounts quickly by filling passwords with one click. +- Share logins easily with your team. + +{{#if InviterEmail}} +This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}. +{{else}} +This invitation expires {{ExpirationDate}}. +{{/if}} + +Your existing account will be owned by {{OrganizationName}} + +By accepting this invitation, your account ({{Email}}) will be owned by {{OrganizationName}} and will be subject to their security and management policies. Contact your administrator with any questions or concerns. + +Accept invitation: {{Url}} +{{/TitleContactUsTextLayout}} diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.html.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.html.hbs new file mode 100644 index 0000000000..59e12e899a --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.html.hbs @@ -0,0 +1,908 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +

+ {{OrganizationName}} set up a Bitwarden password manager account for you. +

+ +
+ + + + + + + +
+ + {{ButtonText}} + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +
+ + +
+ +
+ + + + + +
+ + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
{{OrganizationName}} is rolling out Bitwarden to increase security and protect your sensitive data. Once you finish account setup, you can:
+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Store Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Store logins securely so you never forget your passwords.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Autofill Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Sign in to accounts quickly by filling passwords with one click.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Share Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Share logins easily with your team.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
{{#if InviterEmail}} + This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}} + {{else}} + This invitation expires {{ExpirationDate}} + {{/if}}
+ +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +

+ Learn more about Bitwarden +

+

+ Find user guides, product documentation, and videos on the + Bitwarden Help Center. +

+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + +
+ +

+ © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa + Barbara, CA, USA +

+

+ Always confirm you are on a trusted Bitwarden domain before logging + in:
+ bitwarden.com | + Learn why we include this +

+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.text.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.text.hbs new file mode 100644 index 0000000000..cc0ea9091f --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.text.hbs @@ -0,0 +1,15 @@ +{{#>TitleContactUsTextLayout}} +{{OrganizationName}} is rolling out Bitwarden to increase security and protect your sensitive data. Once you finish account setup, you can: + +- Store logins securely so you never forget your passwords. +- Sign in to accounts quickly by filling passwords with one click. +- Share logins easily with your team. + +{{#if InviterEmail}} +This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}. +{{else}} +This invitation expires {{ExpirationDate}}. +{{/if}} + +Finish account setup: {{Url}} +{{/TitleContactUsTextLayout}} diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.html.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.html.hbs new file mode 100644 index 0000000000..2e76a0fed0 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.html.hbs @@ -0,0 +1,908 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +

+ {{OrganizationName}} invited you to join them on Bitwarden +

+ +
+ + + + + + + +
+ + {{ButtonText}} + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +
+ + +
+ +
+ + + + + +
+ + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
{{OrganizationName}} is using Bitwarden to simplify password sharing and protect your sensitive data. Once you accept this invitation, you can:
+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Store Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Store logins securely so you never forget your passwords.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Autofill Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Sign in to accounts quickly by filling passwords with one click.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Share Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Share logins easily with your friends, family, or coworkers.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
{{#if InviterEmail}} + This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}} + {{else}} + This invitation expires {{ExpirationDate}} + {{/if}}
+ +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +

+ Learn more about Bitwarden +

+

+ Find user guides, product documentation, and videos on the + Bitwarden Help Center. +

+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + +
+ +

+ © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa + Barbara, CA, USA +

+

+ Always confirm you are on a trusted Bitwarden domain before logging + in:
+ bitwarden.com | + Learn why we include this +

+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.text.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.text.hbs new file mode 100644 index 0000000000..a2df4eb43a --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.text.hbs @@ -0,0 +1,15 @@ +{{#>TitleContactUsTextLayout}} +{{OrganizationName}} is using Bitwarden to simplify password sharing and protect your sensitive data. Once you accept this invitation, you can: + +- Store logins securely so you never forget your passwords. +- Sign in to accounts quickly by filling passwords with one click. +- Share logins easily with your friends, family, or coworkers. + +{{#if InviterEmail}} +This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}. +{{else}} +This invitation expires {{ExpirationDate}}. +{{/if}} + +Accept invitation: {{Url}} +{{/TitleContactUsTextLayout}} diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesNewUserView.html.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesNewUserView.html.hbs new file mode 100644 index 0000000000..03d41f12e5 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesNewUserView.html.hbs @@ -0,0 +1,908 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +

+ {{OrganizationName}} set up a Bitwarden password manager account for you. +

+ +
+ + + + + + + +
+ + {{ButtonText}} + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +
+ + +
+ +
+ + + + + +
+ + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
{{OrganizationName}} is using Bitwarden to simplify password sharing and protect your sensitive data. Once you finish account setup, you can:
+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Store Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Store logins securely so you never forget your passwords.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Autofill Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Sign in to accounts quickly by filling passwords with one click.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Share Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Share logins easily with your friends, family, or coworkers.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
{{#if InviterEmail}} + This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}} + {{else}} + This invitation expires {{ExpirationDate}} + {{/if}}
+ +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +

+ Learn more about Bitwarden +

+

+ Find user guides, product documentation, and videos on the + Bitwarden Help Center. +

+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + +
+ +

+ © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa + Barbara, CA, USA +

+

+ Always confirm you are on a trusted Bitwarden domain before logging + in:
+ bitwarden.com | + Learn why we include this +

+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesNewUserView.text.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesNewUserView.text.hbs new file mode 100644 index 0000000000..071a056c40 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesNewUserView.text.hbs @@ -0,0 +1,15 @@ +{{#>TitleContactUsTextLayout}} +{{OrganizationName}} is using Bitwarden to simplify password sharing and protect your sensitive data. Once you finish account setup, you can: + +- Store logins securely so you never forget your passwords. +- Sign in to accounts quickly by filling passwords with one click. +- Share logins easily with your friends, family, or coworkers. + +{{#if InviterEmail}} +This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}. +{{else}} +This invitation expires {{ExpirationDate}}. +{{/if}} + +Finish account setup: {{Url}} +{{/TitleContactUsTextLayout}} diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFreeView.html.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFreeView.html.hbs new file mode 100644 index 0000000000..d7c5acd126 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFreeView.html.hbs @@ -0,0 +1,908 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +

+ You have been invited to Bitwarden Password Manager +

+ +
+ + + + + + + +
+ + {{ButtonText}} + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +
+ + +
+ +
+ + + + + +
+ + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
Bitwarden is a password manager used to simplify password sharing and protect your sensitive data. Once you accept this invitation, you can:
+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Store Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Securely store logins so you never forget your passwords.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Autofill Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Sign in to accounts quickly by filling passwords with one click.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Share Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Share logins easily with your friends, family, or coworkers.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
{{#if InviterEmail}} + This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}} + {{else}} + This invitation expires {{ExpirationDate}} + {{/if}}
+ +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +

+ Learn more about Bitwarden +

+

+ Find user guides, product documentation, and videos on the + Bitwarden Help Center. +

+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + +
+ +

+ © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa + Barbara, CA, USA +

+

+ Always confirm you are on a trusted Bitwarden domain before logging + in:
+ bitwarden.com | + Learn why we include this +

+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFreeView.text.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFreeView.text.hbs new file mode 100644 index 0000000000..fa4b7abaaf --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFreeView.text.hbs @@ -0,0 +1,15 @@ +{{#>TitleContactUsTextLayout}} +Bitwarden is a password manager used to simplify password sharing and protect your sensitive data. Once you accept this invitation, you can: + +- Securely store logins so you never forget your passwords. +- Sign in to accounts quickly by filling passwords with one click. +- Share logins easily with your friends, family, or coworkers. + +{{#if InviterEmail}} +This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}. +{{else}} +This invitation expires {{ExpirationDate}}. +{{/if}} + +Accept invitation: {{Url}} +{{/TitleContactUsTextLayout}} diff --git a/src/Core/Models/Mail/OrganizationInvitesInfo.cs b/src/Core/Models/Mail/OrganizationInvitesInfo.cs index 9ba80d8299..73ee65743a 100644 --- a/src/Core/Models/Mail/OrganizationInvitesInfo.cs +++ b/src/Core/Models/Mail/OrganizationInvitesInfo.cs @@ -15,7 +15,8 @@ public class OrganizationInvitesInfo bool orgSsoLoginRequiredPolicyEnabled, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> orgUserTokenPairs, Dictionary orgUserHasExistingUserDict, - bool initOrganization = false + bool initOrganization = false, + string inviterEmail = null ) { OrganizationName = org.DisplayName(); @@ -30,6 +31,7 @@ public class OrganizationInvitesInfo OrgUserTokenPairs = orgUserTokenPairs; OrgUserHasExistingUserDict = orgUserHasExistingUserDict; + InviterEmail = inviterEmail; } public string OrganizationName { get; } @@ -41,5 +43,6 @@ public class OrganizationInvitesInfo public bool OrgSsoLoginRequiredPolicyEnabled { get; } public IEnumerable<(OrganizationUser OrgUser, ExpiringToken Token)> OrgUserTokenPairs { get; } public Dictionary OrgUserHasExistingUserDict { get; } + public string InviterEmail { get; } } diff --git a/src/Core/Platform/Mail/HandlebarsMailService.cs b/src/Core/Platform/Mail/HandlebarsMailService.cs index 298e335c9f..f1be34d037 100644 --- a/src/Core/Platform/Mail/HandlebarsMailService.cs +++ b/src/Core/Platform/Mail/HandlebarsMailService.cs @@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Models.Mail; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models.Business; using Bit.Core.Auth.Models.Mail; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.Mail; @@ -390,6 +391,65 @@ public class HandlebarsMailService : IMailService } } + public async Task SendUpdatedOrganizationInviteEmailsAsync(OrganizationInvitesInfo orgInvitesInfo) + { + var messageModels = orgInvitesInfo.OrgUserTokenPairs.Select(orgUserTokenPair => + { + Debug.Assert(orgUserTokenPair.OrgUser.Email is not null); + + var userHasExistingUser = orgInvitesInfo.OrgUserHasExistingUserDict[orgUserTokenPair.OrgUser.Id]; + var organizationName = orgInvitesInfo.OrganizationName; + + var (subject, templateName, buttonText) = GetUpdatedInviteTemplateInfo( + orgInvitesInfo.PlanType, userHasExistingUser, organizationName); + + var url = BuildInvitationUrl(orgInvitesInfo, orgUserTokenPair.OrgUser, orgUserTokenPair.Token); + var expirationDate = $"{orgUserTokenPair.Token.ExpirationDate.ToLongDateString()} {orgUserTokenPair.Token.ExpirationDate.ToShortTimeString()} UTC"; + + var message = CreateDefaultMessage(subject, orgUserTokenPair.OrgUser.Email); + + return new MailQueueMessage(message, templateName, new + { + OrganizationName = organizationName, + Email = orgUserTokenPair.OrgUser.Email, + ExpirationDate = expirationDate, + Url = url, + ButtonText = buttonText, + InviterEmail = orgInvitesInfo.InviterEmail, + CurrentYear = DateTime.UtcNow.Year.ToString() + }); + }); + + await EnqueueMailAsync(messageModels); + } + + public async Task SendUpdatedOrganizationConfirmedEmailAsync(Organization organization, string userEmail, bool accessSecretsManager = false) + { + var organizationName = organization.DisplayName(); + var webVaultUrl = accessSecretsManager + ? _globalSettings.BaseServiceUri.VaultWithHashAndSecretManagerProduct + : _globalSettings.BaseServiceUri.VaultWithHash; + + var templateName = IsEnterpriseOrTeamsPlan(organization.PlanType) + ? "AdminConsole.OrganizationConfirmation.OrganizationConfirmationEnterpriseTeamsView" + : "AdminConsole.OrganizationConfirmation.OrganizationConfirmationFamilyFreeView"; + + var message = CreateDefaultMessage($"You can now access items from {organizationName}", userEmail); + + var queueMessage = new MailQueueMessage(message, templateName, new + { + OrganizationName = organizationName, + TitleFirst = "You're confirmed as a member of ", + TitleSecondBold = organizationName, + TitleThird = "!", + WebVaultUrl = webVaultUrl, + CurrentYear = DateTime.UtcNow.Year.ToString() + }); + queueMessage.Category = "OrganizationUserConfirmed"; + + await EnqueueMailAsync(queueMessage); + } + public async Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email) { var message = CreateDefaultMessage($"You have been revoked from {organizationName}", email); @@ -725,6 +785,103 @@ public class HandlebarsMailService : IMailService private Task EnqueueMailAsync(IEnumerable queueMessages) => _mailEnqueuingService.EnqueueManyAsync(queueMessages, SendEnqueuedMailMessageAsync); + private static (string Subject, string TemplateName, string ButtonText) GetUpdatedInviteTemplateInfo( + PlanType planType, bool userHasExistingUser, string organizationName) + { + const string newUserSubject = "set up a Bitwarden account for you"; + const string newUserButton = "Finish account setup"; + const string existingUserSubject = "invited you to their Bitwarden organization"; + const string existingUserButton = "Accept invitation"; + + if (IsEnterpriseOrTeamsPlan(planType)) + { + return userHasExistingUser + ? ($"{organizationName} {existingUserSubject}", + "AdminConsole.OrganizationInvite.OrganizationInviteEnterpriseTeamsExistingUserView", + existingUserButton) + : ($"{organizationName} {newUserSubject}", + "AdminConsole.OrganizationInvite.OrganizationInviteEnterpriseTeamsNewUserView", + newUserButton); + } + + if (IsFamiliesPlan(planType)) + { + return userHasExistingUser + ? ($"{organizationName} {existingUserSubject}", + "AdminConsole.OrganizationInvite.OrganizationInviteFamiliesExistingUserView", + existingUserButton) + : ($"{organizationName} {newUserSubject}", + "AdminConsole.OrganizationInvite.OrganizationInviteFamiliesNewUserView", + newUserButton); + } + + return (userHasExistingUser + ? "You have been invited to a Bitwarden Organization" + : "You have been invited to Bitwarden Password Manager", + "AdminConsole.OrganizationInvite.OrganizationInviteFreeView", + existingUserButton); + } + + private static bool IsEnterpriseOrTeamsPlan(PlanType planType) + { + return planType switch + { + PlanType.TeamsMonthly2019 or + PlanType.TeamsAnnually2019 or + PlanType.TeamsMonthly2020 or + PlanType.TeamsAnnually2020 or + PlanType.TeamsMonthly2023 or + PlanType.TeamsAnnually2023 or + PlanType.TeamsStarter2023 or + PlanType.TeamsMonthly or + PlanType.TeamsAnnually or + PlanType.TeamsStarter or + PlanType.EnterpriseMonthly2019 or + PlanType.EnterpriseAnnually2019 or + PlanType.EnterpriseMonthly2020 or + PlanType.EnterpriseAnnually2020 or + PlanType.EnterpriseMonthly2023 or + PlanType.EnterpriseAnnually2023 or + PlanType.EnterpriseMonthly or + PlanType.EnterpriseAnnually or + PlanType.Custom => true, + _ => false + }; + } + + private static bool IsFamiliesPlan(PlanType planType) + { + return planType switch + { + PlanType.FamiliesAnnually2019 or + PlanType.FamiliesAnnually2025 or + PlanType.FamiliesAnnually => true, + _ => false + }; + } + + private string BuildInvitationUrl(OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token) + { + var baseUrl = $"{_globalSettings.BaseServiceUri.VaultWithHash}/accept-organization"; + var queryParams = new List + { + $"organizationId={orgUser.OrganizationId}", + $"organizationUserId={orgUser.Id}", + $"email={WebUtility.UrlEncode(orgUser.Email)}", + $"organizationName={WebUtility.UrlEncode(orgInvitesInfo.OrganizationName)}", + $"token={WebUtility.UrlEncode(token.Token)}", + $"initOrganization={orgInvitesInfo.InitOrganization}", + $"orgUserHasExistingUser={orgInvitesInfo.OrgUserHasExistingUserDict[orgUser.Id]}" + }; + + if (orgInvitesInfo.OrgSsoEnabled && orgInvitesInfo.OrgSsoLoginRequiredPolicyEnabled) + { + queryParams.Add($"orgSsoIdentifier={orgInvitesInfo.OrgSsoIdentifier}"); + } + + return $"{baseUrl}?{string.Join("&", queryParams)}"; + } + private MailMessage CreateDefaultMessage(string subject, string toEmail) { return CreateDefaultMessage(subject, new List { toEmail }); diff --git a/src/Core/Platform/Mail/IMailService.cs b/src/Core/Platform/Mail/IMailService.cs index e07e4bad29..bd52c2ac3b 100644 --- a/src/Core/Platform/Mail/IMailService.cs +++ b/src/Core/Platform/Mail/IMailService.cs @@ -69,10 +69,12 @@ public interface IMailService /// /// The information required to send the organization invites. Task SendOrganizationInviteEmailsAsync(OrganizationInvitesInfo orgInvitesInfo); + Task SendUpdatedOrganizationInviteEmailsAsync(OrganizationInvitesInfo orgInvitesInfo); Task SendOrganizationMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable ownerEmails); Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable ownerEmails); Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable adminEmails, bool hasAccessSecretsManager = false); Task SendOrganizationConfirmedEmailAsync(string organizationName, string email, bool hasAccessSecretsManager = false); + Task SendUpdatedOrganizationConfirmedEmailAsync(Organization organization, string userEmail, bool accessSecretsManager = false); Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email); Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email); Task SendPasswordlessSignInAsync(string returnUrl, string token, string email); diff --git a/src/Core/Platform/Mail/NoopMailService.cs b/src/Core/Platform/Mail/NoopMailService.cs index 0064058afb..8ec8c00867 100644 --- a/src/Core/Platform/Mail/NoopMailService.cs +++ b/src/Core/Platform/Mail/NoopMailService.cs @@ -78,11 +78,21 @@ public class NoopMailService : IMailService return Task.FromResult(0); } + public Task SendUpdatedOrganizationConfirmedEmailAsync(Organization organization, string userEmail, bool accessSecretsManager = false) + { + return Task.FromResult(0); + } + public Task SendOrganizationInviteEmailsAsync(OrganizationInvitesInfo orgInvitesInfo) { return Task.FromResult(0); } + public Task SendUpdatedOrganizationInviteEmailsAsync(OrganizationInvitesInfo orgInvitesInfo) + { + return Task.FromResult(0); + } + public Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email) => Task.CompletedTask; diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUsers/AutomaticallyConfirmUsersCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUsers/AutomaticallyConfirmUsersCommandTests.cs index 252fb89c87..e2e4c9d419 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUsers/AutomaticallyConfirmUsersCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUsers/AutomaticallyConfirmUsersCommandTests.cs @@ -390,7 +390,7 @@ public class AutomaticallyConfirmUsersCommandTests var emailException = new Exception("Email sending failed"); sutProvider.GetDependency() - .SendOrganizationConfirmedEmailAsync(organization.Name, user.Email, organizationUser.AccessSecretsManager) + .SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email, organizationUser.AccessSecretsManager) .ThrowsAsync(emailException); // Act @@ -705,7 +705,7 @@ public class AutomaticallyConfirmUsersCommandTests await sutProvider.GetDependency() .Received(1) .SendOrganizationConfirmedEmailAsync( - organization.Name, + organization.DisplayName(), user.Email, organizationUser.AccessSecretsManager); @@ -722,7 +722,7 @@ public class AutomaticallyConfirmUsersCommandTests [Theory] [BitAutoData] - public async Task SendOrganizationConfirmedEmailAsync_WithFeatureFlagOn_UsesNewMailer( + public async Task SendOrganizationConfirmedEmailAsync_WithFeatureFlagOn_CallsSendOrganizationConfirmationCommand( Organization organization, string userEmail, SutProvider sutProvider) @@ -741,8 +741,8 @@ public class AutomaticallyConfirmUsersCommandTests .Received(1) .SendConfirmationAsync(organization, userEmail, accessSecretsManager); await sutProvider.GetDependency() - .DidNotReceive() - .SendOrganizationConfirmedEmailAsync(Arg.Any(), Arg.Any(), Arg.Any()); + .DidNotReceiveWithAnyArgs() + .SendOrganizationConfirmedEmailAsync(default, default, default); } [Theory] @@ -764,9 +764,9 @@ public class AutomaticallyConfirmUsersCommandTests // Assert await sutProvider.GetDependency() .Received(1) - .SendOrganizationConfirmedEmailAsync(organization.Name, userEmail, accessSecretsManager); + .SendOrganizationConfirmedEmailAsync(organization.DisplayName(), userEmail, accessSecretsManager); await sutProvider.GetDependency() - .DidNotReceive() - .SendConfirmationAsync(Arg.Any(), Arg.Any(), Arg.Any()); + .DidNotReceiveWithAnyArgs() + .SendConfirmationAsync(default, default, default); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs index a544dd1729..0aed4e2ff5 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs @@ -797,7 +797,7 @@ public class ConfirmOrganizationUserCommandTests } [Theory, BitAutoData] - public async Task SendOrganizationConfirmedEmailAsync_WithFeatureFlagOn_UsesNewMailer( + public async Task SendOrganizationConfirmedEmailAsync_WithFeatureFlagOn_CallsSendOrganizationConfirmationCommand( Organization org, string userEmail, SutProvider sutProvider) @@ -816,8 +816,8 @@ public class ConfirmOrganizationUserCommandTests .Received(1) .SendConfirmationAsync(org, userEmail, accessSecretsManager); await sutProvider.GetDependency() - .DidNotReceive() - .SendOrganizationConfirmedEmailAsync(Arg.Any(), Arg.Any(), Arg.Any()); + .DidNotReceiveWithAnyArgs() + .SendOrganizationConfirmedEmailAsync(default, default, default); } [Theory, BitAutoData] @@ -840,8 +840,8 @@ public class ConfirmOrganizationUserCommandTests .Received(1) .SendOrganizationConfirmedEmailAsync(org.DisplayName(), userEmail, accessSecretsManager); await sutProvider.GetDependency() - .DidNotReceive() - .SendConfirmationAsync(Arg.Any(), Arg.Any(), Arg.Any()); + .DidNotReceiveWithAnyArgs() + .SendConfirmationAsync(default, default, default); } [Theory, BitAutoData] diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommandTests.cs index a4a6858477..60aaa6664a 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommandTests.cs @@ -1,7 +1,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationInvite; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; @@ -11,7 +10,6 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Models.Mail; -using Bit.Core.Platform.Mail.Mailer; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Test.AdminConsole.AutoFixture; @@ -23,7 +21,6 @@ using Bit.Test.Common.Fakes; using NSubstitute; using NSubstitute.ReturnsExtensions; using Xunit; -using CoreGlobalSettings = Bit.Core.Settings.GlobalSettings; namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; @@ -117,15 +114,13 @@ public class SendOrganizationInvitesCommandTests } [Theory] - [BitAutoData(PlanType.EnterpriseAnnually, false)] - [BitAutoData(PlanType.EnterpriseMonthly2023, false)] - [BitAutoData(PlanType.TeamsAnnually, false)] - [BitAutoData(PlanType.TeamsStarter, false)] - [BitAutoData(PlanType.EnterpriseAnnually, true)] - [BitAutoData(PlanType.TeamsAnnually, true)] - public async Task SendInvitesAsync_WithFeatureFlag_EnterpriseAndTeamsPlans_SendsEnterpriseTemplate( + [BitAutoData(PlanType.EnterpriseAnnually)] + [BitAutoData(PlanType.TeamsAnnually)] + [BitAutoData(PlanType.FamiliesAnnually)] + [BitAutoData(PlanType.Free)] + [BitAutoData(PlanType.Custom)] + public async Task SendInvitesAsync_WithFeatureFlagEnabled_CallsMailServiceWithNewTemplates( PlanType planType, - bool userExists, Organization organization, OrganizationUser invite, User invitingUser, @@ -137,107 +132,6 @@ public class SendOrganizationInvitesCommandTests organization.PlanType = planType; invite.OrganizationId = organization.Id; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.UpdateJoinOrganizationEmailTemplate) - .Returns(true); - - sutProvider.GetDependency() - .GetManyByEmailsAsync(Arg.Any>()) - .Returns(userExists ? [new User { Email = invite.Email }] : []); - - sutProvider.GetDependency() - .GetByIdAsync(invitingUser.Id) - .Returns(invitingUser); - - sutProvider.GetDependency() - .CreateToken(Arg.Any()) - .Returns(info => new OrgUserInviteTokenable(info.Arg()) - { - ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) - }); - - // Act - await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, invitingUser.Id)); - - // Assert - if (userExists) - { - await sutProvider.GetDependency().Received(1) - .SendEmail(Arg.Any()); - } - else - { - await sutProvider.GetDependency().Received(1) - .SendEmail(Arg.Any()); - } - } - - [Theory] - [BitAutoData(PlanType.FamiliesAnnually, false)] - [BitAutoData(PlanType.FamiliesAnnually2025, false)] - [BitAutoData(PlanType.FamiliesAnnually, true)] - public async Task SendInvitesAsync_WithFeatureFlag_FamiliesPlans_SendsFamiliesTemplate( - PlanType planType, - bool userExists, - Organization organization, - OrganizationUser invite, - User invitingUser, - SutProvider sutProvider) - { - SetupSutProvider(sutProvider); - - // Arrange - organization.PlanType = planType; - invite.OrganizationId = organization.Id; - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.UpdateJoinOrganizationEmailTemplate) - .Returns(true); - - sutProvider.GetDependency() - .GetManyByEmailsAsync(Arg.Any>()) - .Returns(userExists ? [new User { Email = invite.Email }] : []); - - sutProvider.GetDependency() - .GetByIdAsync(invitingUser.Id) - .Returns(invitingUser); - - sutProvider.GetDependency() - .CreateToken(Arg.Any()) - .Returns(info => new OrgUserInviteTokenable(info.Arg()) - { - ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) - }); - - // Act - await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, invitingUser.Id)); - - // Assert - if (userExists) - { - await sutProvider.GetDependency().Received(1) - .SendEmail(Arg.Any()); - } - else - { - await sutProvider.GetDependency().Received(1) - .SendEmail(Arg.Any()); - } - } - - [Theory, BitAutoData] - public async Task SendInvitesAsync_WithFeatureFlag_FreePlan_SendsFreeTemplate( - Organization organization, - OrganizationUser invite, - User invitingUser, - SutProvider sutProvider) - { - SetupSutProvider(sutProvider); - - // Arrange - organization.PlanType = PlanType.Free; - invite.OrganizationId = organization.Id; - sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.UpdateJoinOrganizationEmailTemplate) .Returns(true); @@ -261,91 +155,10 @@ public class SendOrganizationInvitesCommandTests await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, invitingUser.Id)); // Assert - await sutProvider.GetDependency().Received(1) - .SendEmail(Arg.Any()); - } - - [Theory, BitAutoData] - public async Task SendInvitesAsync_WithFeatureFlag_CustomPlan_SendsEnterpriseTemplate( - Organization organization, - OrganizationUser invite, - User invitingUser, - SutProvider sutProvider) - { - SetupSutProvider(sutProvider); - - // Arrange - organization.PlanType = PlanType.Custom; - invite.OrganizationId = organization.Id; - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.UpdateJoinOrganizationEmailTemplate) - .Returns(true); - - sutProvider.GetDependency() - .GetManyByEmailsAsync(Arg.Any>()) - .Returns([]); - - sutProvider.GetDependency() - .GetByIdAsync(invitingUser.Id) - .Returns(invitingUser); - - sutProvider.GetDependency() - .CreateToken(Arg.Any()) - .Returns(info => new OrgUserInviteTokenable(info.Arg()) - { - ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) - }); - - // Act - await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, invitingUser.Id)); - - // Assert - await sutProvider.GetDependency().Received(1) - .SendEmail(Arg.Any()); - } - - [Theory, BitAutoData] - public async Task SendInvitesAsync_WithFeatureFlagEnabled_UsesNewMailer( - Organization organization, - OrganizationUser invite, - User invitingUser, - SutProvider sutProvider) - { - SetupSutProvider(sutProvider); - - // Arrange - organization.PlanType = PlanType.EnterpriseAnnually; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.UpdateJoinOrganizationEmailTemplate) - .Returns(true); - - sutProvider.GetDependency() - .GetManyByEmailsAsync(Arg.Any>()) - .Returns([new User { Email = invite.Email }]); - - sutProvider.GetDependency() - .GetByIdAsync(invitingUser.Id) - .Returns(invitingUser); - - sutProvider.GetDependency() - .CreateToken(Arg.Any()) - .Returns(info => new OrgUserInviteTokenable(info.Arg()) - { - ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) - }); - - // Act - await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, invitingUser.Id)); - - // Assert - verify new mailer is called, not legacy mail service - await sutProvider.GetDependency() - .Received(1) - .SendEmail(Arg.Any()); - - await sutProvider.GetDependency() - .DidNotReceive() - .SendOrganizationInviteEmailsAsync(Arg.Any()); + await sutProvider.GetDependency().Received(1) + .SendUpdatedOrganizationInviteEmailsAsync(Arg.Is(info => + info.OrgUserTokenPairs.Any(p => p.OrgUser.Email == invite.Email) && + info.InviterEmail == invitingUser.Email)); } [Theory, BitAutoData] @@ -377,10 +190,6 @@ public class SendOrganizationInvitesCommandTests await sutProvider.GetDependency() .Received(1) .SendOrganizationInviteEmailsAsync(Arg.Any()); - - await sutProvider.GetDependency() - .DidNotReceive() - .SendEmail(Arg.Any>()); } [Theory, BitAutoData] @@ -417,9 +226,10 @@ public class SendOrganizationInvitesCommandTests await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, invitingUser.Id)); // Assert - await sutProvider.GetDependency().Received(1) - .SendEmail(Arg.Is(mail => - mail.View.InviterEmail == invitingUser.Email)); + await sutProvider.GetDependency().Received(1) + .SendUpdatedOrganizationInviteEmailsAsync(Arg.Is(info => + info.OrgUserTokenPairs.Any(p => p.OrgUser.Email == invite.Email) && + info.InviterEmail == invitingUser.Email)); } [Theory, BitAutoData] @@ -451,9 +261,10 @@ public class SendOrganizationInvitesCommandTests await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, null)); // Assert - await sutProvider.GetDependency().Received(1) - .SendEmail(Arg.Is(mail => - mail.View.InviterEmail == null)); + await sutProvider.GetDependency().Received(1) + .SendUpdatedOrganizationInviteEmailsAsync(Arg.Is(info => + info.OrgUserTokenPairs.Any(p => p.OrgUser.Email == invite.Email) && + info.InviterEmail == null)); } [Theory, BitAutoData] @@ -491,15 +302,15 @@ public class SendOrganizationInvitesCommandTests await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, nonExistentUserId)); // Assert - await sutProvider.GetDependency().Received(1) - .SendEmail(Arg.Is(mail => - mail.View.InviterEmail == null)); + await sutProvider.GetDependency().Received(1) + .SendUpdatedOrganizationInviteEmailsAsync(Arg.Is(info => + info.OrgUserTokenPairs.Any(p => p.OrgUser.Email == invite.Email) && + info.InviterEmail == null)); } private void SetupSutProvider(SutProvider sutProvider) { sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); - sutProvider.SetDependency(new CoreGlobalSettings { BaseServiceUri = new CoreGlobalSettings.BaseServiceUriSettings(new CoreGlobalSettings()) }, "globalSettings"); sutProvider.Create(); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/SendOrganizationConfirmationCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/SendOrganizationConfirmationCommandTests.cs index 9e8ecb76d4..b36fb746d8 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/SendOrganizationConfirmationCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/SendOrganizationConfirmationCommandTests.cs @@ -1,8 +1,7 @@ using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationConfirmation; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation; using Bit.Core.Billing.Enums; -using Bit.Core.Platform.Mail.Mailer; +using Bit.Core.Services; using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -16,230 +15,39 @@ public class SendOrganizationConfirmationCommandTests { [Theory] [OrganizationCustomize, BitAutoData] - public async Task SendConfirmationAsync_EnterpriseOrganization_SendsEnterpriseTeamsEmail( + public async Task SendConfirmationAsync_EnterpriseOrg_CallsUpdatedConfirmedEmail( Organization organization, string userEmail, SutProvider sutProvider) { // Arrange organization.PlanType = PlanType.EnterpriseAnnually; - organization.Name = "Test Enterprise Org"; // Act - await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false); + await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, true); // Assert - await sutProvider.GetDependency().Received(1) - .SendEmail(Arg.Is(mail => - mail.ToEmails.Contains(userEmail) && - mail.ToEmails.Count() == 1 && - mail.View.OrganizationName == organization.Name && - mail.Subject == GetSubject(organization.Name))); + await sutProvider.GetDependency() + .Received(1) + .SendUpdatedOrganizationConfirmedEmailAsync(organization, userEmail, true); } [Theory] [OrganizationCustomize, BitAutoData] - public async Task SendConfirmationAsync_TeamsOrganization_SendsEnterpriseTeamsEmail( - Organization organization, - string userEmail, - SutProvider sutProvider) - { - // Arrange - organization.PlanType = PlanType.TeamsAnnually; - organization.Name = "Test Teams Org"; - - // Act - await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false); - - // Assert - await sutProvider.GetDependency().Received(1) - .SendEmail(Arg.Is(mail => - mail.ToEmails.Contains(userEmail) && - mail.ToEmails.Count() == 1 && - mail.View.OrganizationName == organization.Name && - mail.Subject == GetSubject(organization.Name))); - } - - [Theory] - [OrganizationCustomize, BitAutoData] - public async Task SendConfirmationAsync_FamilyOrganization_SendsFamilyFreeEmail( + public async Task SendConfirmationAsync_FamiliesOrg_CallsUpdatedConfirmedEmail( Organization organization, string userEmail, SutProvider sutProvider) { // Arrange organization.PlanType = PlanType.FamiliesAnnually; - organization.Name = "Test Family Org"; // Act await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false); // Assert - await sutProvider.GetDependency().Received(1) - .SendEmail(Arg.Is(mail => - mail.ToEmails.Contains(userEmail) && - mail.ToEmails.Count() == 1 && - mail.View.OrganizationName == organization.Name && - mail.Subject == GetSubject(organization.Name))); + await sutProvider.GetDependency() + .Received(1) + .SendUpdatedOrganizationConfirmedEmailAsync(organization, userEmail, false); } - - [Theory] - [OrganizationCustomize, BitAutoData] - public async Task SendConfirmationAsync_FreeOrganization_SendsFamilyFreeEmail( - Organization organization, - string userEmail, - SutProvider sutProvider) - { - // Arrange - organization.PlanType = PlanType.Free; - organization.Name = "Test Free Org"; - - // Act - await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false); - - // Assert - await sutProvider.GetDependency().Received(1) - .SendEmail(Arg.Is(mail => - mail.ToEmails.Contains(userEmail) && - mail.ToEmails.Count() == 1 && - mail.View.OrganizationName == organization.Name && - mail.Subject == GetSubject(organization.Name))); - } - - [Theory] - [OrganizationCustomize, BitAutoData] - public async Task SendConfirmationsAsync_MultipleUsers_SendsSingleEmail( - Organization organization, - List userEmails, - SutProvider sutProvider) - { - // Arrange - organization.PlanType = PlanType.EnterpriseAnnually; - organization.Name = "Test Enterprise Org"; - - // Act - await sutProvider.Sut.SendConfirmationsAsync(organization, userEmails, false); - - // Assert - await sutProvider.GetDependency().Received(1) - .SendEmail(Arg.Is(mail => - mail.ToEmails.SequenceEqual(userEmails) && - mail.View.OrganizationName == organization.Name)); - } - - [Theory] - [OrganizationCustomize, BitAutoData] - public async Task SendConfirmationsAsync_EmptyUserList_DoesNotSendEmail( - Organization organization, - SutProvider sutProvider) - { - // Arrange - organization.PlanType = PlanType.EnterpriseAnnually; - organization.Name = "Test Enterprise Org"; - - // Act - await sutProvider.Sut.SendConfirmationsAsync(organization, [], false); - - // Assert - await sutProvider.GetDependency().DidNotReceive() - .SendEmail(Arg.Any()); - await sutProvider.GetDependency().DidNotReceive() - .SendEmail(Arg.Any()); - } - - [Theory] - [OrganizationCustomize, BitAutoData] - public async Task SendConfirmationAsync_HtmlEncodedOrganizationName_DecodesNameCorrectly( - Organization organization, - string userEmail, - SutProvider sutProvider) - { - // Arrange - organization.PlanType = PlanType.EnterpriseAnnually; - organization.Name = "Test & Company"; - var expectedDecodedName = "Test & Company"; - - // Act - await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false); - - // Assert - await sutProvider.GetDependency().Received(1) - .SendEmail(Arg.Is(mail => - mail.View.OrganizationName == expectedDecodedName)); - } - - [Theory] - [OrganizationCustomize, BitAutoData] - public async Task SendConfirmationAsync_AllEnterpriseTeamsPlanTypes_SendsEnterpriseTeamsEmail( - Organization organization, - string userEmail, - SutProvider sutProvider) - { - // Test all Enterprise and Teams plan types - var enterpriseTeamsPlanTypes = new[] - { - PlanType.TeamsMonthly2019, PlanType.TeamsAnnually2019, - PlanType.TeamsMonthly2020, PlanType.TeamsAnnually2020, - PlanType.TeamsMonthly2023, PlanType.TeamsAnnually2023, - PlanType.TeamsStarter2023, PlanType.TeamsMonthly, - PlanType.TeamsAnnually, PlanType.TeamsStarter, - PlanType.EnterpriseMonthly2019, PlanType.EnterpriseAnnually2019, - PlanType.EnterpriseMonthly2020, PlanType.EnterpriseAnnually2020, - PlanType.EnterpriseMonthly2023, PlanType.EnterpriseAnnually2023, - PlanType.EnterpriseMonthly, PlanType.EnterpriseAnnually - }; - - foreach (var planType in enterpriseTeamsPlanTypes) - { - // Arrange - organization.PlanType = planType; - organization.Name = "Test Org"; - sutProvider.GetDependency().ClearReceivedCalls(); - - // Act - await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false); - - // Assert - await sutProvider.GetDependency().Received(1) - .SendEmail(Arg.Any()); - await sutProvider.GetDependency().DidNotReceive() - .SendEmail(Arg.Any()); - } - } - - [Theory] - [OrganizationCustomize, BitAutoData] - public async Task SendConfirmationAsync_AllFamilyFreePlanTypes_SendsFamilyFreeEmail( - Organization organization, - string userEmail, - SutProvider sutProvider) - { - // Test all Family, Free, and Custom plan types - var familyFreePlanTypes = new[] - { - PlanType.Free, PlanType.FamiliesAnnually2019, - PlanType.FamiliesAnnually2025, PlanType.FamiliesAnnually, - PlanType.Custom - }; - - foreach (var planType in familyFreePlanTypes) - { - // Arrange - organization.PlanType = planType; - organization.Name = "Test Org"; - sutProvider.GetDependency().ClearReceivedCalls(); - - // Act - await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false); - - // Assert - await sutProvider.GetDependency().Received(1) - .SendEmail(Arg.Any()); - await sutProvider.GetDependency().DidNotReceive() - .SendEmail(Arg.Any()); - } - } - - private static string GetSubject(string organizationName) => $"You can now access items from {organizationName}"; - }