diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs
index 5a600e26bf..f7ce3aa59e 100644
--- a/src/Core/Constants.cs
+++ b/src/Core/Constants.cs
@@ -154,6 +154,7 @@ public static class FeatureFlagKeys
public const string DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods";
public const string PM23174ManageAccountRecoveryPermissionDrivesTheNeedToSetMasterPassword =
"pm-23174-manage-account-recovery-permission-drives-the-need-to-set-master-password";
+ public const string MJMLBasedEmailTemplates = "mjml-based-email-templates";
/* Autofill Team */
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
diff --git a/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs b/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs
new file mode 100644
index 0000000000..095cdc82d7
--- /dev/null
+++ b/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs
@@ -0,0 +1,675 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ Verify your email to access this Bitwarden Send
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ Your verification code is:
+
+ |
+
+
+
+ |
+
+ {{Token}}
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+ This code expires in {{Expiry}} minutes. After that, you'll need to
+ verify your email again.
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bitwarden Send transmits sensitive, temporary information to
+ others easily and securely. Learn more about
+ Bitwarden Send
+ or
+ sign up
+ to try it today.
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Learn more about Bitwarden
+
+ Find user guides, product documentation, and videos on the
+ Bitwarden Help Center.
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.text.hbs b/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.text.hbs
new file mode 100644
index 0000000000..7c9c1db527
--- /dev/null
+++ b/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.text.hbs
@@ -0,0 +1,9 @@
+{{#>BasicTextLayout}}
+Verify your email to access this Bitwarden Send.
+
+Your verification code is: {{Token}}
+
+This code can only be used once and expires in {{Expiry}} minutes. After that you'll need to verify your email again.
+
+Bitwarden Send transmits sensitive, temporary information to others easily and securely. Learn more about Bitwarden Send or sign up to try it today.
+{{/BasicTextLayout}}
diff --git a/src/Core/MailTemplates/Mjml/.mjmlconfig b/src/Core/MailTemplates/Mjml/.mjmlconfig
index 7560e0fb96..c382f10a12 100644
--- a/src/Core/MailTemplates/Mjml/.mjmlconfig
+++ b/src/Core/MailTemplates/Mjml/.mjmlconfig
@@ -1,5 +1,5 @@
{
"packages": [
- "components/hero"
+ "components/mj-bw-hero"
]
}
diff --git a/src/Core/MailTemplates/Mjml/components/footer.mjml b/src/Core/MailTemplates/Mjml/components/footer.mjml
index 0634033618..2b2268f33b 100644
--- a/src/Core/MailTemplates/Mjml/components/footer.mjml
+++ b/src/Core/MailTemplates/Mjml/components/footer.mjml
@@ -2,38 +2,38 @@
@@ -45,8 +45,8 @@
Always confirm you are on a trusted Bitwarden domain before logging
in:
- bitwarden.com |
- Learn why we include this
+ bitwarden.com |
+ Learn why we include this
diff --git a/src/Core/MailTemplates/Mjml/components/head.mjml b/src/Core/MailTemplates/Mjml/components/head.mjml
index 389ae77c12..cf78cd6223 100644
--- a/src/Core/MailTemplates/Mjml/components/head.mjml
+++ b/src/Core/MailTemplates/Mjml/components/head.mjml
@@ -4,7 +4,7 @@
font-size="16px"
/>
-
+
@@ -22,3 +22,9 @@
border-radius: 3px;
}
+
+
+
+@media only screen and
+ (max-width: 480px) { .hide-small-img { display: none !important; } .send-bubble { padding-left: 20px; padding-right: 20px; width: 90% !important; } }
+
diff --git a/src/Core/MailTemplates/Mjml/components/hero.js b/src/Core/MailTemplates/Mjml/components/hero.js
deleted file mode 100644
index 6c5bd9bc99..0000000000
--- a/src/Core/MailTemplates/Mjml/components/hero.js
+++ /dev/null
@@ -1,64 +0,0 @@
-const { BodyComponent } = require("mjml-core");
-class MjBwHero extends BodyComponent {
- static dependencies = {
- // Tell the validator which tags are allowed as our component's parent
- "mj-column": ["mj-bw-hero"],
- "mj-wrapper": ["mj-bw-hero"],
- // Tell the validator which tags are allowed as our component's children
- "mj-bw-hero": [],
- };
-
- static allowedAttributes = {
- "img-src": "string",
- title: "string",
- "button-text": "string",
- "button-url": "string",
- };
-
- static defaultAttributes = {};
-
- render() {
- return this.renderMJML(`
-
-
-
-
-
- ${this.getAttribute("title")}
-
-
-
- ${this.getAttribute("button-text")}
-
-
-
-
-
-
- `);
- }
-}
-
-module.exports = MjBwHero;
diff --git a/src/Core/MailTemplates/Mjml/components/learn-more-footer.mjml b/src/Core/MailTemplates/Mjml/components/learn-more-footer.mjml
new file mode 100644
index 0000000000..9df0614aae
--- /dev/null
+++ b/src/Core/MailTemplates/Mjml/components/learn-more-footer.mjml
@@ -0,0 +1,18 @@
+
+
+
+
+ Learn more about Bitwarden
+
+ Find user guides, product documentation, and videos on the
+ Bitwarden Help Center.
+
+
+
+
+
+
diff --git a/src/Core/MailTemplates/Mjml/components/mj-bw-hero.js b/src/Core/MailTemplates/Mjml/components/mj-bw-hero.js
new file mode 100644
index 0000000000..d329d4ea38
--- /dev/null
+++ b/src/Core/MailTemplates/Mjml/components/mj-bw-hero.js
@@ -0,0 +1,100 @@
+const { BodyComponent } = require("mjml-core");
+class MjBwHero extends BodyComponent {
+ static dependencies = {
+ // Tell the validator which tags are allowed as our component's parent
+ "mj-column": ["mj-bw-hero"],
+ "mj-wrapper": ["mj-bw-hero"],
+ // Tell the validator which tags are allowed as our component's children
+ "mj-bw-hero": [],
+ };
+
+ static allowedAttributes = {
+ "img-src": "string", // REQUIRED: Source for the image displayed in the right-hand side of the blue header area
+ title: "string", // REQUIRED: large text stating primary purpose of the email
+ "button-text": "string", // OPTIONAL: text to display in the button
+ "button-url": "string", // OPTIONAL: URL to navigate to when the button is clicked
+ "sub-title": "string", // OPTIONAL: smaller text providing additional context for the title
+ };
+
+ static defaultAttributes = {};
+
+ render() {
+ if (this.getAttribute("button-text") && this.getAttribute("button-url")) {
+ return this.renderMJML(`
+
+
+
+
+
+ ${this.getAttribute("title")}
+
+
+
+ ${this.getAttribute("button-text")}
+
+
+
+
+
+
+ `);
+ } else {
+ return this.renderMJML(`
+
+
+
+
+
+ ${this.getAttribute("title")}
+
+
+
+
+
+
+
+ `);
+ }
+ }
+}
+
+module.exports = MjBwHero;
diff --git a/src/Core/MailTemplates/Mjml/emails/Auth/send-email-otp.mjml b/src/Core/MailTemplates/Mjml/emails/Auth/send-email-otp.mjml
new file mode 100644
index 0000000000..6ccc481ff8
--- /dev/null
+++ b/src/Core/MailTemplates/Mjml/emails/Auth/send-email-otp.mjml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Your verification code is:
+ {{Token}}
+
+
+ This code expires in {{Expiry}} minutes. After that, you'll need to
+ verify your email again.
+
+
+
+
+
+
+
+ Bitwarden Send transmits sensitive, temporary information to
+ others easily and securely. Learn more about
+ Bitwarden Send
+ or
+ sign up
+ to try it today.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Core/MailTemplates/Mjml/emails/two-factor.mjml b/src/Core/MailTemplates/Mjml/emails/Auth/two-factor.mjml
similarity index 77%
rename from src/Core/MailTemplates/Mjml/emails/two-factor.mjml
rename to src/Core/MailTemplates/Mjml/emails/Auth/two-factor.mjml
index 5091e208d3..3b63c278fc 100644
--- a/src/Core/MailTemplates/Mjml/emails/two-factor.mjml
+++ b/src/Core/MailTemplates/Mjml/emails/Auth/two-factor.mjml
@@ -1,10 +1,10 @@
-
+
-
+
-
+
diff --git a/src/Core/MailTemplates/Mjml/emails/invite.mjml b/src/Core/MailTemplates/Mjml/emails/invite.mjml
index 4eae12d0dc..cdace39c95 100644
--- a/src/Core/MailTemplates/Mjml/emails/invite.mjml
+++ b/src/Core/MailTemplates/Mjml/emails/invite.mjml
@@ -22,26 +22,7 @@
-
-
-
-
- We’re here for you!
-
- If you have any questions, search the Bitwarden
- Help
- site or
- contact us.
-
-
-
-
-
-
+
diff --git a/src/Core/Models/Mail/Auth/DefaultEmailOtpViewModel.cs b/src/Core/Models/Mail/Auth/DefaultEmailOtpViewModel.cs
index 5faf550e60..5eabd5ba2c 100644
--- a/src/Core/Models/Mail/Auth/DefaultEmailOtpViewModel.cs
+++ b/src/Core/Models/Mail/Auth/DefaultEmailOtpViewModel.cs
@@ -9,4 +9,5 @@ public class DefaultEmailOtpViewModel : BaseMailModel
public string? TheDate { get; set; }
public string? TheTime { get; set; }
public string? TimeZone { get; set; }
+ public string? Expiry { get; set; }
}
diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs
index 6e61c4f8dd..5a3428c25a 100644
--- a/src/Core/Services/IMailService.cs
+++ b/src/Core/Services/IMailService.cs
@@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
+using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations;
@@ -31,6 +32,16 @@ public interface IMailService
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose);
Task SendSendEmailOtpEmailAsync(string email, string token, string subject);
+ ///
+ /// has a default expiry of 5 minutes so we set the expiry to that value int he view model.
+ /// Sends OTP code token to the specified email address.
+ /// will replace when MJML templates are fully accepted.
+ ///
+ /// Email address to send the OTP to
+ /// Otp code token
+ /// subject line of the email
+ /// Task
+ Task SendSendEmailOtpEmailv2Async(string email, string token, string subject);
Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType type, DateTime utcNow, string ip);
Task SendNoMasterPasswordHintEmailAsync(string email);
Task SendMasterPasswordHintEmailAsync(string email, string hint);
diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs
index 75e0c78702..19705766ed 100644
--- a/src/Core/Services/Implementations/HandlebarsMailService.cs
+++ b/src/Core/Services/Implementations/HandlebarsMailService.cs
@@ -224,6 +224,27 @@ public class HandlebarsMailService : IMailService
await _mailDeliveryService.SendEmailAsync(message);
}
+ public async Task SendSendEmailOtpEmailv2Async(string email, string token, string subject)
+ {
+ var message = CreateDefaultMessage(subject, email);
+ var requestDateTime = DateTime.UtcNow;
+ var model = new DefaultEmailOtpViewModel
+ {
+ Token = token,
+ Expiry = "5", // This should be configured through the OTPDefaultTokenProviderOptions but for now we will hardcode it to 5 minutes.
+ TheDate = requestDateTime.ToLongDateString(),
+ TheTime = requestDateTime.ToShortTimeString(),
+ TimeZone = _utcTimeZoneDisplay,
+ WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
+ SiteName = _globalSettings.SiteName,
+ };
+ await AddMessageContentAsync(message, "Auth.SendAccessEmailOtpEmailv2", model);
+ message.MetaData.Add("SendGridBypassListManagement", true);
+ // TODO - PM-25380 change to string constant
+ message.Category = "SendEmailOtp";
+ await _mailDeliveryService.SendEmailAsync(message);
+ }
+
public async Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip)
{
// Check if we've sent this email within the last hour
diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs
index 7ec05bb1f9..1459fab966 100644
--- a/src/Core/Services/NoopImplementations/NoopMailService.cs
+++ b/src/Core/Services/NoopImplementations/NoopMailService.cs
@@ -98,6 +98,11 @@ public class NoopMailService : IMailService
return Task.FromResult(0);
}
+ public Task SendSendEmailOtpEmailv2Async(string email, string token, string subject)
+ {
+ return Task.FromResult(0);
+ }
+
public Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip)
{
return Task.FromResult(0);
diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs
index ca48c4fbec..34a7a6f6e7 100644
--- a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs
+++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs
@@ -1,4 +1,5 @@
using System.Security.Claims;
+using Bit.Core;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Services;
@@ -10,6 +11,7 @@ using Duende.IdentityServer.Validation;
namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess;
public class SendEmailOtpRequestValidator(
+ IFeatureService featureService,
IOtpTokenProvider otpTokenProvider,
IMailService mailService) : ISendAuthenticationMethodValidator
{
@@ -60,11 +62,20 @@ public class SendEmailOtpRequestValidator(
{
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.OtpGenerationFailed);
}
-
- await mailService.SendSendEmailOtpEmailAsync(
- email,
- token,
- string.Format(SendAccessConstants.OtpEmail.Subject, token));
+ if (featureService.IsEnabled(FeatureFlagKeys.MJMLBasedEmailTemplates))
+ {
+ await mailService.SendSendEmailOtpEmailv2Async(
+ email,
+ token,
+ string.Format(SendAccessConstants.OtpEmail.Subject, token));
+ }
+ else
+ {
+ await mailService.SendSendEmailOtpEmailAsync(
+ email,
+ token,
+ string.Format(SendAccessConstants.OtpEmail.Subject, token));
+ }
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailOtpSent);
}
diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs
index 46f61cb333..7fdfacf428 100644
--- a/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs
+++ b/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs
@@ -265,9 +265,10 @@ public class SendEmailOtpRequestValidatorTests
// Arrange
var otpTokenProvider = Substitute.For>();
var mailService = Substitute.For();
+ var featureService = Substitute.For();
// Act
- var validator = new SendEmailOtpRequestValidator(otpTokenProvider, mailService);
+ var validator = new SendEmailOtpRequestValidator(featureService, otpTokenProvider, mailService);
// Assert
Assert.NotNull(validator);