mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
Added conditional subject and button text to invite email. (#6304)
* Added conditional subject and button text to invite email. * Added feature flag.
This commit is contained in:
@@ -13,9 +13,9 @@ public static class AuthorizationHandlerCollectionExtensions
|
||||
|
||||
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>(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
@@ -71,12 +72,15 @@ public class SendOrganizationInvitesCommand(
|
||||
|
||||
var orgUsersWithExpTokens = orgUsers.Select(MakeOrgUserExpiringTokenPair);
|
||||
|
||||
var isSubjectFeatureEnabled = featureService.IsEnabled(FeatureFlagKeys.InviteEmailImprovements);
|
||||
|
||||
return new OrganizationInvitesInfo(
|
||||
organization,
|
||||
orgSsoEnabled,
|
||||
orgSsoLoginRequiredPolicyEnabled,
|
||||
orgUsersWithExpTokens,
|
||||
orgUserHasExistingUserDict,
|
||||
isSubjectFeatureEnabled,
|
||||
initOrganization
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -15,6 +15,7 @@ public class OrganizationInvitesInfo
|
||||
bool orgSsoLoginRequiredPolicyEnabled,
|
||||
IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> orgUserTokenPairs,
|
||||
Dictionary<Guid, bool> orgUserHasExistingUserDict,
|
||||
bool isSubjectFeatureEnabled = false,
|
||||
bool initOrganization = false
|
||||
)
|
||||
{
|
||||
@@ -29,6 +30,8 @@ public class OrganizationInvitesInfo
|
||||
|
||||
OrgUserTokenPairs = orgUserTokenPairs;
|
||||
OrgUserHasExistingUserDict = orgUserHasExistingUserDict;
|
||||
|
||||
IsSubjectFeatureEnabled = isSubjectFeatureEnabled;
|
||||
}
|
||||
|
||||
public string OrganizationName { get; }
|
||||
@@ -38,6 +41,8 @@ public class 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; }
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ public class OrganizationUserInvitedViewModel : BaseTitleContactUsMailModel
|
||||
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 ",
|
||||
@@ -48,6 +49,45 @@ public class OrganizationUserInvitedViewModel : BaseTitleContactUsMailModel
|
||||
};
|
||||
}
|
||||
|
||||
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; }
|
||||
@@ -60,6 +100,8 @@ public class OrganizationUserInvitedViewModel : BaseTitleContactUsMailModel
|
||||
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
|
||||
{
|
||||
|
||||
@@ -351,21 +351,43 @@ public class HandlebarsMailService : IMailService
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user