From 2e3e96a25cc8903f3d57a67b8bac22be8bd3d720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bispo?= Date: Tue, 21 Mar 2023 14:44:58 +0000 Subject: [PATCH] [PM-1095][PM-1104] Update email template (#2746) * [SG-994] Add import Open Sans font to full template * [SG-994] Update organization user invite email template to new UI * [SG-994] update alt text for mobile app download buttons * [SG-994] Update copy. Add hyperlinks to stores. * [SG-944] Improve layout responsiveness * [PM-1095][PM-1104] Add new template for title and contact us. Add new template for user organization invite * [PM-1095][PM-1104] Remove wrong text from free invite * [PM-1104][PM-1095] Add bold class. Add margin. * [PM-1104][PM-1095] Change font type to previously used * [PM-1104][PM-1095] Remove Open Sans font * [PM-1104][PM-1095] Improve browsers rendering compatibility * [PM-1104][PM-1095] Fixed margins * [PM-1095][PM-1104] Remove unnecessary string sanitise. --- .../Handlebars/Layouts/Full.html.hbs | 165 ++++++++++++------ .../Layouts/TitleContactUs.html.hbs | 26 +++ .../Layouts/TitleContactUs.text.hbs | 11 ++ .../OrganizationUserConfirmed.html.hbs | 39 +++-- .../OrganizationUserConfirmed.text.hbs | 8 +- .../OrganizationUserInvited.html.hbs | 39 ++--- .../OrganizationUserInvited.text.hbs | 11 +- .../Mail/BaseTitleContactUsMailModel.cs | 9 + .../OrganizationUserConfirmedViewModel.cs | 2 +- .../Mail/OrganizationUserInvitedViewModel.cs | 2 +- src/Core/Services/IMailService.cs | 4 +- .../Implementations/HandlebarsMailService.cs | 20 ++- .../Implementations/OrganizationService.cs | 5 +- .../NoopImplementations/NoopMailService.cs | 4 +- .../Services/OrganizationServiceTests.cs | 10 +- 15 files changed, 232 insertions(+), 123 deletions(-) create mode 100644 src/Core/MailTemplates/Handlebars/Layouts/TitleContactUs.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/Layouts/TitleContactUs.text.hbs create mode 100644 src/Core/Models/Mail/BaseTitleContactUsMailModel.cs diff --git a/src/Core/MailTemplates/Handlebars/Layouts/Full.html.hbs b/src/Core/MailTemplates/Handlebars/Layouts/Full.html.hbs index bf884604f..2655324a6 100644 --- a/src/Core/MailTemplates/Handlebars/Layouts/Full.html.hbs +++ b/src/Core/MailTemplates/Handlebars/Layouts/Full.html.hbs @@ -5,7 +5,6 @@ Bitwarden - {{! Yahoo center fix }} - + +
- {{! 600px container }} - - - {{! Left column (center fix) }} - + {{! Right column (center fix) }} + +
- - - - -
- - - + +
+ + +
+ {{! 600px container }} + + + {{! Left column (center fix) }} + - {{! Right column (center fix) }} - -
+ + + + +
+ + + - -
- {{>@partial-block}} + {{>@partial-block}} -
- - - - - - - - -
-
+
+ + + + + + + + +
+
diff --git a/src/Core/MailTemplates/Handlebars/Layouts/TitleContactUs.html.hbs b/src/Core/MailTemplates/Handlebars/Layouts/TitleContactUs.html.hbs new file mode 100644 index 000000000..962ca564e --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/Layouts/TitleContactUs.html.hbs @@ -0,0 +1,26 @@ +{{#>FullHtmlLayout}} +
+
+
+
+ {{TitleFirst}}{{TitleSecondBold}}{{TitleThird}} +
+
+
+ +
+
+ + {{>@partial-block}} + +
+ +
+ +
+
+
+{{/FullHtmlLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/Layouts/TitleContactUs.text.hbs b/src/Core/MailTemplates/Handlebars/Layouts/TitleContactUs.text.hbs new file mode 100644 index 000000000..bf0aa7478 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/Layouts/TitleContactUs.text.hbs @@ -0,0 +1,11 @@ +{{#>FullTextLayout}} +{{TitleFirst}} {{TitleSecondBold}} {{TitleThird}} + +{{>@partial-block}} + + +We’re here for you! +If you have any questions, search the Bitwarden Help site or contact us. +- https://bitwarden.com/help/ +- https://bitwarden.com/contact/ +{{/FullTextLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserConfirmed.html.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserConfirmed.html.hbs index 81b6628c7..1cef15232 100644 --- a/src/Core/MailTemplates/Handlebars/OrganizationUserConfirmed.html.hbs +++ b/src/Core/MailTemplates/Handlebars/OrganizationUserConfirmed.html.hbs @@ -1,14 +1,25 @@ -{{#>FullHtmlLayout}} - - - - - - - -
- This email is to notify you that you have been confirmed as a user of {{OrganizationName}}. -
- Any collections and logins being shared with you by this organization will now appear in your Bitwarden vault. -
-{{/FullHtmlLayout}} +{{#>TitleContactUsHtmlLayout}} +
+
+ You may now access logins and other items this organizations has shared with you from your Bitwarden vault. +
+
+
+
+ + Go to vault + +
+
+
+
+ Tip: Use the Bitwarden mobile app to quickly save logins and auto-fill forms. Download from the App Store or Google Play. +
+
+
+
+ Android download + iOS download +
+
+{{/TitleContactUsHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserConfirmed.text.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserConfirmed.text.hbs index 7a7c1a498..f728c2b15 100644 --- a/src/Core/MailTemplates/Handlebars/OrganizationUserConfirmed.text.hbs +++ b/src/Core/MailTemplates/Handlebars/OrganizationUserConfirmed.text.hbs @@ -1,5 +1,5 @@ -{{#>BasicTextLayout}} -This email is to notify you that you have been confirmed as a user of {{OrganizationName}}. +{{#>TitleContactUsTextLayout}} +You may now access logins and other items this organizations has shared with you from your Bitwarden vault. -Any collections and logins being shared with you by this organization will now appear in your Bitwarden vault. -{{/BasicTextLayout}} \ No newline at end of file +Tip: Use the Bitwarden mobile app to quickly save logins and auto-fill forms. Download from the App Store or Google Play. +{{/TitleContactUsTextLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs index 01a642a59..8db383b19 100644 --- a/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs +++ b/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs @@ -1,24 +1,15 @@ -{{#>FullHtmlLayout}} - - - - - - - - - - -
- You have been invited to join the {{OrganizationName}} organization. This link expires on {{ExpirationDate}}. -
- If you do not wish to join this organization, you can safely ignore this email. -
-
-
- - Join Organization Now - -
-
-{{/FullHtmlLayout}} +{{#>TitleContactUsHtmlLayout}} +
+
+ + Join Organization Now + +
+
+
+
+ This invitation expires on {{ExpirationDate}} +
+
+{{/TitleContactUsHtmlLayout}} + diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.text.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.text.hbs index e7f2b9da2..e06284074 100644 --- a/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.text.hbs +++ b/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.text.hbs @@ -1,10 +1,5 @@ -{{#>BasicTextLayout}} -You have been invited to join the {{OrganizationName}} organization. - -This link expires on {{ExpirationDate}}. - -If you do not wish to join this organization, you can safely ignore this email. - +{{#>TitleContactUsTextLayout}} {{{Url}}} -{{/BasicTextLayout}} +This invitation expires on {{ExpirationDate}}. +{{/TitleContactUsTextLayout}} diff --git a/src/Core/Models/Mail/BaseTitleContactUsMailModel.cs b/src/Core/Models/Mail/BaseTitleContactUsMailModel.cs new file mode 100644 index 000000000..a04831265 --- /dev/null +++ b/src/Core/Models/Mail/BaseTitleContactUsMailModel.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Models.Mail; + +public class BaseTitleContactUsMailModel : BaseMailModel +{ + public string TitleFirst { get; set; } + public string TitleSecondBold { get; set; } + public string TitleThird { get; set; } +} + diff --git a/src/Core/Models/Mail/OrganizationUserConfirmedViewModel.cs b/src/Core/Models/Mail/OrganizationUserConfirmedViewModel.cs index 61e710774..8254d3d84 100644 --- a/src/Core/Models/Mail/OrganizationUserConfirmedViewModel.cs +++ b/src/Core/Models/Mail/OrganizationUserConfirmedViewModel.cs @@ -1,6 +1,6 @@ namespace Bit.Core.Models.Mail; -public class OrganizationUserConfirmedViewModel : BaseMailModel +public class OrganizationUserConfirmedViewModel : BaseTitleContactUsMailModel { public string OrganizationName { get; set; } } diff --git a/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs b/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs index 4bf9fbb86..cdd550aea 100644 --- a/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs +++ b/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs @@ -1,6 +1,6 @@ namespace Bit.Core.Models.Mail; -public class OrganizationUserInvitedViewModel : BaseMailModel +public class OrganizationUserInvitedViewModel : BaseTitleContactUsMailModel { public string OrganizationName { get; set; } public string OrganizationId { get; set; } diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 9c86a445d..bac0b58b7 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -15,8 +15,8 @@ public interface IMailService Task SendTwoFactorEmailAsync(string email, string token); Task SendNoMasterPasswordHintEmailAsync(string email); Task SendMasterPasswordHintEmailAsync(string email, string hint); - Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token); - Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites); + Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token, bool isFreeOrg); + Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites, bool isFreeOrg); Task SendOrganizationMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable ownerEmails); Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable ownerEmails); Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable adminEmails); diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 89edb7f09..e15067840 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -191,6 +191,9 @@ public class HandlebarsMailService : IMailService var message = CreateDefaultMessage($"You Have Been Confirmed To {organizationName}", email); var model = new OrganizationUserConfirmedViewModel { + TitleFirst = "You're confirmed as a member of ", + TitleSecondBold = CoreHelpers.SanitizeForEmail(organizationName, false), + TitleThird = "!", OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false), WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, SiteName = _globalSettings.SiteName @@ -200,21 +203,24 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } - public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token) => - BulkSendOrganizationInviteEmailAsync(organizationName, new[] { (orgUser, token) }); + public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token, bool isFreeOrg) => + BulkSendOrganizationInviteEmailAsync(organizationName, new[] { (orgUser, token) }, isFreeOrg); - public async Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites) + public async Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites, bool isFreeOrg) { MailQueueMessage CreateMessage(string email, object model) { var message = CreateDefaultMessage($"Join {organizationName}", email); return new MailQueueMessage(message, "OrganizationUserInvited", model); } - + var freeOrgTitle = "A Bitwarden member invited you to an organization. Join now to start securing your passwords!"; var messageModels = invites.Select(invite => CreateMessage(invite.orgUser.Email, new OrganizationUserInvitedViewModel { - OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false), + TitleFirst = isFreeOrg ? freeOrgTitle : "Join ", + TitleSecondBold = isFreeOrg ? string.Empty : CoreHelpers.SanitizeForEmail(organizationName, false), + TitleThird = isFreeOrg ? string.Empty : " on Bitwarden and start securing your passwords!", + OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false) + invite.orgUser.Status, Email = WebUtility.UrlEncode(invite.orgUser.Email), OrganizationId = invite.orgUser.OrganizationId.ToString(), OrganizationUserId = invite.orgUser.Id.ToString(), @@ -478,6 +484,10 @@ public class HandlebarsMailService : IMailService Handlebars.RegisterTemplate("FullHtmlLayout", fullHtmlLayoutSource); var fullTextLayoutSource = await ReadSourceAsync("Layouts.Full.text"); Handlebars.RegisterTemplate("FullTextLayout", fullTextLayoutSource); + var titleContactUsHtmlLayoutSource = await ReadSourceAsync("Layouts.TitleContactUs.html"); + Handlebars.RegisterTemplate("TitleContactUsHtmlLayout", titleContactUsHtmlLayoutSource); + var titleContactUsTextLayoutSource = await ReadSourceAsync("Layouts.TitleContactUs.text"); + Handlebars.RegisterTemplate("TitleContactUsTextLayout", titleContactUsTextLayoutSource); Handlebars.RegisterHelper("date", (writer, context, parameters) => { diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 0c0154c2d..9c428b614 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -1193,7 +1193,7 @@ public class OrganizationService : IOrganizationService _dataProtector.Protect($"OrganizationUserInvite {orgUser.Id} {orgUser.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}"); await _mailService.BulkSendOrganizationInviteEmailAsync(organization.Name, - orgUsers.Select(o => (o, new ExpiringToken(MakeToken(o), DateTime.UtcNow.AddDays(5))))); + orgUsers.Select(o => (o, new ExpiringToken(MakeToken(o), DateTime.UtcNow.AddDays(5)))), organization.PlanType == PlanType.Free); } private async Task SendInviteAsync(OrganizationUser orgUser, Organization organization) @@ -1202,8 +1202,7 @@ public class OrganizationService : IOrganizationService var nowMillis = CoreHelpers.ToEpocMilliseconds(now); var token = _dataProtector.Protect( $"OrganizationUserInvite {orgUser.Id} {orgUser.Email} {nowMillis}"); - - await _mailService.SendOrganizationInviteEmailAsync(organization.Name, orgUser, new ExpiringToken(token, now.AddDays(5))); + await _mailService.SendOrganizationInviteEmailAsync(organization.Name, orgUser, new ExpiringToken(token, now.AddDays(5)), organization.PlanType == PlanType.Free); } public async Task AcceptUserAsync(Guid organizationUserId, User user, string token, diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index 9d7b4698a..07dd0a39f 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -52,12 +52,12 @@ public class NoopMailService : IMailService return Task.FromResult(0); } - public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token) + public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token, bool isFreeOrg) { return Task.FromResult(0); } - public Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites) + public Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites, bool isFreeOrg) { return Task.FromResult(0); } diff --git a/test/Core.Test/Services/OrganizationServiceTests.cs b/test/Core.Test/Services/OrganizationServiceTests.cs index 6bc60b85e..ee8fdc8fb 100644 --- a/test/Core.Test/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/Services/OrganizationServiceTests.cs @@ -64,7 +64,7 @@ public class OrganizationServiceTests .CreateManyAsync(Arg.Is>(users => users.Count() == expectedNewUsersCount)); await sutProvider.GetDependency().Received(1) .BulkSendOrganizationInviteEmailAsync(org.Name, - Arg.Is>(messages => messages.Count() == expectedNewUsersCount)); + Arg.Is>(messages => messages.Count() == expectedNewUsersCount), org.PlanType == PlanType.Free); // Send events await sutProvider.GetDependency().Received(1) @@ -122,7 +122,7 @@ public class OrganizationServiceTests .CreateManyAsync(Arg.Is>(users => users.Count() == expectedNewUsersCount)); await sutProvider.GetDependency().Received(1) .BulkSendOrganizationInviteEmailAsync(org.Name, - Arg.Is>(messages => messages.Count() == expectedNewUsersCount)); + Arg.Is>(messages => messages.Count() == expectedNewUsersCount), org.PlanType == PlanType.Free); // Sent events await sutProvider.GetDependency().Received(1) @@ -217,7 +217,7 @@ public class OrganizationServiceTests await sutProvider.GetDependency().Received(1) .BulkSendOrganizationInviteEmailAsync(organization.Name, - Arg.Is>(v => v.Count() == invite.Emails.Distinct().Count())); + Arg.Is>(v => v.Count() == invite.Emails.Distinct().Count()), organization.PlanType == PlanType.Free); } [Theory] @@ -460,7 +460,7 @@ public class OrganizationServiceTests await sutProvider.GetDependency().Received(1) .BulkSendOrganizationInviteEmailAsync(organization.Name, - Arg.Is>(v => v.Count() == invites.SelectMany(i => i.invite.Emails).Count())); + Arg.Is>(v => v.Count() == invites.SelectMany(i => i.invite.Emails).Count()), organization.PlanType == PlanType.Free); await sutProvider.GetDependency().Received(1).LogOrganizationUserEventsAsync(Arg.Any>()); } @@ -494,7 +494,7 @@ public class OrganizationServiceTests await sutProvider.GetDependency().Received(1) .BulkSendOrganizationInviteEmailAsync(organization.Name, - Arg.Is>(v => v.Count() == invites.SelectMany(i => i.invite.Emails).Count())); + Arg.Is>(v => v.Count() == invites.SelectMany(i => i.invite.Emails).Count()), organization.PlanType == PlanType.Free); await sutProvider.GetDependency().Received(1).LogOrganizationUserEventsAsync(Arg.Any>()); }