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
Expand Up @@ -13,9 +13,9 @@ public static void AddAdminConsoleAuthorizationHandlers(this IServiceCollection

services.TryAddEnumerable([
ServiceDescriptor.Scoped<IAuthorizationHandler, BulkCollectionAuthorizationHandler>(),
ServiceDescriptor.Scoped<IAuthorizationHandler, CollectionAuthorizationHandler>(),
ServiceDescriptor.Scoped<IAuthorizationHandler, GroupAuthorizationHandler>(),
ServiceDescriptor.Scoped<IAuthorizationHandler, OrganizationRequirementHandler>(),
]);
ServiceDescriptor.Scoped<IAuthorizationHandler, CollectionAuthorizationHandler>(),
ServiceDescriptor.Scoped<IAuthorizationHandler, GroupAuthorizationHandler>(),
ServiceDescriptor.Scoped<IAuthorizationHandler, OrganizationRequirementHandler>(),
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public class SendOrganizationInvitesCommand(
IPolicyRepository policyRepository,
IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory,
IDataProtectorTokenFactory<OrgUserInviteTokenable> dataProtectorTokenFactory,
IMailService mailService) : ISendOrganizationInvitesCommand
IMailService mailService,
IFeatureService featureService) : ISendOrganizationInvitesCommand
{
public async Task SendInvitesAsync(SendInvitesRequest request)
{
Expand Down Expand Up @@ -71,12 +72,15 @@ private async Task<OrganizationInvitesInfo> BuildOrganizationInvitesInfoAsync(IE

var orgUsersWithExpTokens = orgUsers.Select(MakeOrgUserExpiringTokenPair);

var isSubjectFeatureEnabled = featureService.IsEnabled(FeatureFlagKeys.InviteEmailImprovements);

return new OrganizationInvitesInfo(
organization,
orgSsoEnabled,
orgSsoLoginRequiredPolicyEnabled,
orgUsersWithExpTokens,
orgUserHasExistingUserDict,
isSubjectFeatureEnabled,
initOrganization
);
}
Expand Down
1 change: 1 addition & 0 deletions src/Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ public static class FeatureFlagKeys
public const string CipherRepositoryBulkResourceCreation = "pm-24951-cipher-repository-bulk-resource-creation-service";
public const string CollectionVaultRefactor = "pm-25030-resolve-ts-upgrade-errors";
public const string DeleteClaimedUserAccountRefactor = "pm-25094-refactor-delete-managed-organization-user-command";
public const string InviteEmailImprovements = "pm-25644-update-join-organization-subject-line";

/* Auth Team */
public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<tr>
<td display="display: table-cell">
<a href="{{{Url}}}" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
Join Organization Now
{{JoinOrganizationButtonText}}
</a>
</td>
</tr>
Expand Down
5 changes: 5 additions & 0 deletions src/Core/Models/Mail/OrganizationInvitesInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public OrganizationInvitesInfo(
bool orgSsoLoginRequiredPolicyEnabled,
IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> orgUserTokenPairs,
Dictionary<Guid, bool> orgUserHasExistingUserDict,
bool isSubjectFeatureEnabled = false,
bool initOrganization = false
)
{
Expand All @@ -29,6 +30,8 @@ public OrganizationInvitesInfo(

OrgUserTokenPairs = orgUserTokenPairs;
OrgUserHasExistingUserDict = orgUserHasExistingUserDict;

IsSubjectFeatureEnabled = isSubjectFeatureEnabled;
}

public string OrganizationName { get; }
Expand All @@ -38,6 +41,8 @@ public OrganizationInvitesInfo(
public string OrgSsoIdentifier { get; }
public bool OrgSsoLoginRequiredPolicyEnabled { get; }

public bool IsSubjectFeatureEnabled { get; }

public IEnumerable<(OrganizationUser OrgUser, ExpiringToken Token)> OrgUserTokenPairs { get; }
public Dictionary<Guid, bool> OrgUserHasExistingUserDict { get; }

Expand Down
42 changes: 42 additions & 0 deletions src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public static OrganizationUserInvitedViewModel CreateFromInviteInfo(
GlobalSettings globalSettings)
{
var freeOrgTitle = "A Bitwarden member invited you to an organization. Join now to start securing your passwords!";

return new OrganizationUserInvitedViewModel
{
TitleFirst = orgInvitesInfo.IsFreeOrg ? freeOrgTitle : "Join ",
Expand All @@ -48,6 +49,45 @@ public static OrganizationUserInvitedViewModel CreateFromInviteInfo(
};
}

public static OrganizationUserInvitedViewModel CreateFromInviteInfo_v2(
OrganizationInvitesInfo orgInvitesInfo,
OrganizationUser orgUser,
ExpiringToken expiringToken,
GlobalSettings globalSettings)
{
const string freeOrgTitle = "A Bitwarden member invited you to an organization. " +
"Join now to start securing your passwords!";

var userHasExistingUser = orgInvitesInfo.OrgUserHasExistingUserDict[orgUser.Id];

return new OrganizationUserInvitedViewModel
{
TitleFirst = orgInvitesInfo.IsFreeOrg ? freeOrgTitle : "Join ",
TitleSecondBold =
orgInvitesInfo.IsFreeOrg
? string.Empty
: CoreHelpers.SanitizeForEmail(orgInvitesInfo.OrganizationName, false),
TitleThird = orgInvitesInfo.IsFreeOrg ? string.Empty : " on Bitwarden and start securing your passwords!",
OrganizationName = CoreHelpers.SanitizeForEmail(orgInvitesInfo.OrganizationName, false),
Email = WebUtility.UrlEncode(orgUser.Email),
OrganizationId = orgUser.OrganizationId.ToString(),
OrganizationUserId = orgUser.Id.ToString(),
Token = WebUtility.UrlEncode(expiringToken.Token),
ExpirationDate =
$"{expiringToken.ExpirationDate.ToLongDateString()} {expiringToken.ExpirationDate.ToShortTimeString()} UTC",
OrganizationNameUrlEncoded = WebUtility.UrlEncode(orgInvitesInfo.OrganizationName),
WebVaultUrl = globalSettings.BaseServiceUri.VaultWithHash,
SiteName = globalSettings.SiteName,
InitOrganization = orgInvitesInfo.InitOrganization,
OrgSsoIdentifier = orgInvitesInfo.OrgSsoIdentifier,
OrgSsoEnabled = orgInvitesInfo.OrgSsoEnabled,
OrgSsoLoginRequiredPolicyEnabled = orgInvitesInfo.OrgSsoLoginRequiredPolicyEnabled,
OrgUserHasExistingUser = userHasExistingUser,
JoinOrganizationButtonText = userHasExistingUser || orgInvitesInfo.IsFreeOrg ? "Accept invitation" : "Finish account setup",
IsFreeOrg = orgInvitesInfo.IsFreeOrg
};
}

public string OrganizationName { get; set; }
public string OrganizationId { get; set; }
public string OrganizationUserId { get; set; }
Expand All @@ -60,6 +100,8 @@ public static OrganizationUserInvitedViewModel CreateFromInviteInfo(
public bool OrgSsoEnabled { get; set; }
public bool OrgSsoLoginRequiredPolicyEnabled { get; set; }
public bool OrgUserHasExistingUser { get; set; }
public string JoinOrganizationButtonText { get; set; } = "Join Organization";
public bool IsFreeOrg { get; set; }

public string Url
{
Expand Down
38 changes: 30 additions & 8 deletions src/Core/Services/Implementations/HandlebarsMailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -351,21 +351,43 @@ public async Task SendOrganizationConfirmedEmailAsync(string organizationName, s

public async Task SendOrganizationInviteEmailsAsync(OrganizationInvitesInfo orgInvitesInfo)
{
MailQueueMessage CreateMessage(string email, object model)
{
var message = CreateDefaultMessage($"Join {orgInvitesInfo.OrganizationName}", email);
return new MailQueueMessage(message, "OrganizationUserInvited", model);
}

var messageModels = orgInvitesInfo.OrgUserTokenPairs.Select(orgUserTokenPair =>
{
Debug.Assert(orgUserTokenPair.OrgUser.Email is not null);
var orgUserInviteViewModel = OrganizationUserInvitedViewModel.CreateFromInviteInfo(
orgInvitesInfo, orgUserTokenPair.OrgUser, orgUserTokenPair.Token, _globalSettings);

var orgUserInviteViewModel = orgInvitesInfo.IsSubjectFeatureEnabled
? OrganizationUserInvitedViewModel.CreateFromInviteInfo_v2(
orgInvitesInfo, orgUserTokenPair.OrgUser, orgUserTokenPair.Token, _globalSettings)
: OrganizationUserInvitedViewModel.CreateFromInviteInfo(orgInvitesInfo, orgUserTokenPair.OrgUser,
orgUserTokenPair.Token, _globalSettings);

return CreateMessage(orgUserTokenPair.OrgUser.Email, orgUserInviteViewModel);
});

await EnqueueMailAsync(messageModels);
return;

MailQueueMessage CreateMessage(string email, OrganizationUserInvitedViewModel model)
{
var subject = $"Join {model.OrganizationName}";

if (orgInvitesInfo.IsSubjectFeatureEnabled)
{
ArgumentNullException.ThrowIfNull(model);

subject = model! switch
{
{ IsFreeOrg: true, OrgUserHasExistingUser: true } => "You have been invited to a Bitwarden Organization",
{ IsFreeOrg: true, OrgUserHasExistingUser: false } => "You have been invited to Bitwarden Password Manager",
{ IsFreeOrg: false, OrgUserHasExistingUser: true } => $"{model.OrganizationName} invited you to their Bitwarden organization",
{ IsFreeOrg: false, OrgUserHasExistingUser: false } => $"{model.OrganizationName} set up a Bitwarden account for you"
};
}

var message = CreateDefaultMessage(subject, email);

return new MailQueueMessage(message, "OrganizationUserInvited", model);
}
}

public async Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email)
Expand Down
Loading