Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
๏ปฟusing Bit.Core.Platform.Mail.Mailer;

namespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationConfirmation;

public abstract class OrganizationConfirmationBaseView : BaseMailView
{
public required string OrganizationName { get; set; }
public required string TitleFirst { get; set; }
public required string TitleSecondBold { get; set; }
public required string TitleThird { get; set; }
public required string WebVaultUrl { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
๏ปฟusing Bit.Core.Platform.Mail.Mailer;

namespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationConfirmation;

public class OrganizationConfirmationEnterpriseTeamsView : OrganizationConfirmationBaseView
{
}

public class OrganizationConfirmationEnterpriseTeams : BaseMail<OrganizationConfirmationEnterpriseTeamsView>
{
public override required string Subject { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@
<tbody>
<tr>
<td align="center" bgcolor="#ffffff" role="presentation" style="border:none;border-radius:20px;cursor:auto;mso-padding-alt:10px 25px;background:#ffffff;" valign="middle">
<a href="https://vault.bitwarden.com" style="display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:20px;" target="_blank">
<a href="{{{WebVaultUrl}}}" style="display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:20px;" target="_blank">
Log in
</a>
</td>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
๏ปฟusing Bit.Core.Platform.Mail.Mailer;

namespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationConfirmation;

public class OrganizationConfirmationFamilyFreeView : OrganizationConfirmationBaseView
{
}

public class OrganizationConfirmationFamilyFree : BaseMail<OrganizationConfirmationFamilyFreeView>
{
public override required string Subject { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@
<tbody>
<tr>
<td align="center" bgcolor="#ffffff" role="presentation" style="border:none;border-radius:20px;cursor:auto;mso-padding-alt:10px 25px;background:#ffffff;" valign="middle">
<a href="https://vault.bitwarden.com" style="display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:20px;" target="_blank">
<a href="{{{WebVaultUrl}}}" style="display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:20px;" target="_blank">
Log in
</a>
</td>
Expand Down
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ViewModel names need to match the templates. Our templates use kebab-case (MJML), while the ViewModels use PascalCase. We should standardize these conventions, but this is outside the AC teamโ€™s scope. The owner of the mailer and MJML should decide this.

I donโ€™t recommend creating a script to map one naming convention to another. That adds complexity and could introduce more issues over time.

File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
๏ปฟusing Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
๏ปฟusing Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.Entities;
Expand All @@ -25,6 +27,8 @@ public class AutomaticallyConfirmOrganizationUserCommand(IOrganizationUserReposi
IPushNotificationService pushNotificationService,
IPolicyRequirementQuery policyRequirementQuery,
ICollectionRepository collectionRepository,
IFeatureService featureService,
ISendOrganizationConfirmationCommand sendOrganizationConfirmationCommand,
TimeProvider timeProvider,
ILogger<AutomaticallyConfirmOrganizationUserCommand> logger) : IAutomaticallyConfirmOrganizationUserCommand
{
Expand Down Expand Up @@ -143,9 +147,7 @@ private async Task SendConfirmedOrganizationUserEmailAsync(AutomaticallyConfirmO
{
var user = await userRepository.GetByIdAsync(request.OrganizationUser!.UserId!.Value);

await mailService.SendOrganizationConfirmedEmailAsync(request.Organization!.Name,
user!.Email,
request.OrganizationUser.AccessSecretsManager);
await SendOrganizationConfirmedEmailAsync(request.Organization!, user!.Email, request.OrganizationUser.AccessSecretsManager);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -183,4 +185,23 @@ private async Task<AutomaticallyConfirmOrganizationUserValidationRequest> Retrie
Organization = await organizationRepository.GetByIdAsync(request.OrganizationId)
};
}

/// <summary>
/// Sends the organization confirmed email using either the new mailer pattern or the legacy mail service,
/// depending on the feature flag.
/// </summary>
/// <param name="organization">The organization the user was confirmed to.</param>
/// <param name="userEmail">The email address of the confirmed user.</param>
/// <param name="accessSecretsManager">Whether the user has access to Secrets Manager.</param>
internal async Task SendOrganizationConfirmedEmailAsync(Organization organization, string userEmail, bool accessSecretsManager)
{
if (featureService.IsEnabled(FeatureFlagKeys.OrganizationConfirmationEmail))
{
await sendOrganizationConfirmationCommand.SendConfirmationAsync(organization, userEmail, accessSecretsManager);
}
else
{
await mailService.SendOrganizationConfirmedEmailAsync(organization.Name, userEmail, accessSecretsManager);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
๏ปฟ// FIXME: Update this file to be null safe and then delete the line below
#nullable disable

using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
Expand Down Expand Up @@ -35,7 +37,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
private readonly IFeatureService _featureService;
private readonly ICollectionRepository _collectionRepository;
private readonly IAutomaticUserConfirmationPolicyEnforcementValidator _automaticUserConfirmationPolicyEnforcementValidator;

private readonly ISendOrganizationConfirmationCommand _sendOrganizationConfirmationCommand;
public ConfirmOrganizationUserCommand(
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
Expand All @@ -50,7 +52,7 @@ public ConfirmOrganizationUserCommand(
IPolicyRequirementQuery policyRequirementQuery,
IFeatureService featureService,
ICollectionRepository collectionRepository,
IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator)
IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator, ISendOrganizationConfirmationCommand sendOrganizationConfirmationCommand)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
Expand All @@ -66,8 +68,8 @@ public ConfirmOrganizationUserCommand(
_featureService = featureService;
_collectionRepository = collectionRepository;
_automaticUserConfirmationPolicyEnforcementValidator = automaticUserConfirmationPolicyEnforcementValidator;
_sendOrganizationConfirmationCommand = sendOrganizationConfirmationCommand;
}

public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
Guid confirmingUserId, string defaultUserCollectionName = null)
{
Expand Down Expand Up @@ -170,7 +172,7 @@ private async Task<List<Tuple<OrganizationUser, string>>> SaveChangesToDatabaseA
orgUser.Email = null;

await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email, orgUser.AccessSecretsManager);
await SendOrganizationConfirmedEmailAsync(organization, user.Email, orgUser.AccessSecretsManager);
succeededUsers.Add(orgUser);
result.Add(Tuple.Create(orgUser, ""));
}
Expand Down Expand Up @@ -339,4 +341,23 @@ private async Task CreateManyDefaultCollectionsAsync(Guid organizationId,

await _collectionRepository.UpsertDefaultCollectionsAsync(organizationId, eligibleOrganizationUserIds, defaultUserCollectionName);
}

/// <summary>
/// Sends the organization confirmed email using either the new mailer pattern or the legacy mail service,
/// depending on the feature flag.
/// </summary>
/// <param name="organization">The organization the user was confirmed to.</param>
/// <param name="userEmail">The email address of the confirmed user.</param>
/// <param name="accessSecretsManager">Whether the user has access to Secrets Manager.</param>
internal async Task SendOrganizationConfirmedEmailAsync(Organization organization, string userEmail, bool accessSecretsManager)
{
if (_featureService.IsEnabled(FeatureFlagKeys.OrganizationConfirmationEmail))
{
await _sendOrganizationConfirmationCommand.SendConfirmationAsync(organization, userEmail, accessSecretsManager);
}
else
{
await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), userEmail, accessSecretsManager);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
๏ปฟusing Bit.Core.AdminConsole.Entities;

namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;

public interface ISendOrganizationConfirmationCommand
{
/// <summary>
/// Sends an organization confirmation email to the specified user.
/// </summary>
/// <param name="organization">The organization to send the confirmation email for.</param>
/// <param name="userEmail">The email address of the user to send the confirmation to.</param>
/// <param name="accessSecretsManager">Whether the user has access to Secrets Manager.</param>
Task SendConfirmationAsync(Organization organization, string userEmail, bool accessSecretsManager);

/// <summary>
/// Sends organization confirmation emails to multiple users.
/// </summary>
/// <param name="organization">The organization to send the confirmation emails for.</param>
/// <param name="userEmails">The email addresses of the users to send confirmations to.</param>
/// <param name="accessSecretsManager">Whether the users have access to Secrets Manager.</param>
Task SendConfirmationsAsync(Organization organization, IEnumerable<string> userEmails, bool accessSecretsManager);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
๏ปฟ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;

namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;

public class SendOrganizationConfirmationCommand(IMailer mailer, GlobalSettings globalSettings) : ISendOrganizationConfirmationCommand
{
private const string _titleFirst = "You're confirmed as a member of ";
private const string _titleThird = "!";

private static string GetConfirmationSubject(string organizationName) =>
$"You Have Been Confirmed To {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<string> 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<string> 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<string> 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
};
}
}
1 change: 1 addition & 0 deletions src/Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ public static class FeatureFlagKeys
"pm-23174-manage-account-recovery-permission-drives-the-need-to-set-master-password";
public const string MJMLBasedEmailTemplates = "mjml-based-email-templates";
public const string MjmlWelcomeEmailTemplates = "pm-21741-mjml-welcome-email";
public const string OrganizationConfirmationEmail = "pm-28402-update-confirmed-to-org-email-template";
public const string MarketingInitiatedPremiumFlow = "pm-26140-marketing-initiated-premium-flow";
public const string RedirectOnSsoRequired = "pm-1632-redirect-on-sso-required";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
img-src="https://assets.bitwarden.com/email/v1/spot-enterprise.png"
title="You can now share passwords with members of {{OrganizationName}}!"
button-text="Log in"
button-url="https://vault.bitwarden.com"
button-url="{{WebVaultUrl}}"
/>
</mj-wrapper>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
img-src="https://assets.bitwarden.com/email/v1/spot-family-homes.png"
title="You can now share passwords with members of {{OrganizationName}}!"
button-text="Log in"
button-url="https://vault.bitwarden.com"
button-url="{{WebVaultUrl}}"
/>
</mj-wrapper>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ public class Families2019RenewalMailView : BaseMailView

public class Families2019RenewalMail : BaseMail<Families2019RenewalMailView>
{
public override string Subject { get => "Your Bitwarden Families renewal is updating"; }
public override string Subject { get; set; } = "Your Bitwarden Families renewal is updating";
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ public class Families2020RenewalMailView : BaseMailView

public class Families2020RenewalMail : BaseMail<Families2020RenewalMailView>
{
public override string Subject { get => "Your Bitwarden Families renewal is updating"; }
public override string Subject { get; set; } = "Your Bitwarden Families renewal is updating";
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ public class PremiumRenewalMailView : BaseMailView

public class PremiumRenewalMail : BaseMail<PremiumRenewalMailView>
{
public override string Subject { get => "Your Bitwarden Premium renewal is updating"; }
public override string Subject { get; set; } = "Your Bitwarden Premium renewal is updating";
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser;
using Bit.Core.Models.Business.Tokenables;
Expand All @@ -45,7 +46,6 @@
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

using V1_RevokeUsersCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
using V2_RevokeUsersCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;

Expand Down Expand Up @@ -140,6 +140,7 @@ private static void AddOrganizationUserCommands(this IServiceCollection services
services.AddScoped<IUpdateOrganizationUserCommand, UpdateOrganizationUserCommand>();
services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();
services.AddScoped<IConfirmOrganizationUserCommand, ConfirmOrganizationUserCommand>();
services.AddScoped<ISendOrganizationConfirmationCommand, SendOrganizationConfirmationCommand>();
services.AddScoped<IAdminRecoverAccountCommand, AdminRecoverAccountCommand>();
services.AddScoped<IAutomaticallyConfirmOrganizationUserCommand, AutomaticallyConfirmOrganizationUserCommand>();
services.AddScoped<IAutomaticallyConfirmOrganizationUsersValidator, AutomaticallyConfirmOrganizationUsersValidator>();
Expand Down
Loading
Loading