1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-25 12:45:18 +01:00

Initial scaffolding of emails (#1686)

* Initial scaffolding of emails

* Work on adding models for FamilyForEnterprise emails

* Switch verbage

* Put preliminary copy in emails

* Skip test
This commit is contained in:
Justin Baur 2021-11-08 11:47:58 -05:00
parent 005774102a
commit 143be4273b
22 changed files with 393 additions and 0 deletions

View File

@ -0,0 +1,16 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{OrganizationName}}</b> has offered to sponsor a family organization for you with Bitwarden.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
<a href="{{{Url}}}" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; 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;">
Redeem
</a>
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -0,0 +1,5 @@
{{#>BasicTextLayout}}
{{OrganizationName}} has offered to sponsor a family organization for you with Bitwarden. To redeem please click the following link:
{{Url}}
{{/BasicTextLayout}}

View File

@ -0,0 +1,9 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
You have redeemed a family organization sponsorship from {{OrganizationName}}.
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -0,0 +1,3 @@
{{#>BasicTextLayout}}
You have redeemed a family organization sponsorship from {{OrganizationName}}.
{{/BasicTextLayout}}

View File

@ -0,0 +1,9 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
You have redeemed a Families for Enterprise sponsorship from {{OrganizationName}}.
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -0,0 +1,3 @@
{{#>BasicTextLayout}}
A user in your organization has redeemed a family invitation.
{{/BasicTextLayout}}

View File

@ -0,0 +1,16 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
Your Families for Enterprise sponsorship requires reconfirmation.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
<a href="{{{Url}}}" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; 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;">
Reconfirm
</a>
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -0,0 +1,5 @@
{{#>BasicTextLayout}}
Your Families for Enterprise sponsorship requires reconfirmation. To redeem please click the following link:
{{Url}}
{{/BasicTextLayout}}

View File

@ -0,0 +1,9 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
Your Families for Enterprise sponsorship has ended and you will lose premium access at the end of the current billing cycle.
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -0,0 +1,3 @@
{{#>BasicTextLayout}}
Your Families for Enterprise sponsorship has ended and you will lose premium access at the end of the current billing cycle.
{{/BasicTextLayout}}

View File

@ -0,0 +1,9 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
Your Families for Enterprise sponsorship will revert back to your existing payment method at the end of the current billing cycle.
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -0,0 +1,3 @@
{{#>BasicTextLayout}}
Your Families for Enterprise sponsorship will revert back to your existing payment method at the end of the current billing cycle.
{{/BasicTextLayout}}

View File

@ -0,0 +1,8 @@
namespace Bit.Core.Models.Mail.FamiliesForEnterprise
{
public class FamiliesForEnterpriseInviteRedeemableViewModel : BaseMailModel
{
public string OrganizationName { get; set; }
public string Url { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Models.Mail.FamiliesForEnterprise
{
public class FamiliesForEnterpriseInviteRedeemedToFamilyUserViewModel : BaseMailModel
{
public string OrganizationName { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Models.Mail.FamiliesForEnterprise
{
public class FamiliesForEnterpriseInviteRedeemedToOrgUserViewModel : BaseMailModel
{
public string OrganizationName { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Models.Mail.FamiliesForEnterprise
{
public class FamiliesForEnterpriseReconfirmationRequiredViewModel : BaseMailModel
{
}
}

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Models.Mail.FamiliesForEnterprise
{
public class FamiliesForEnterpriseSponsorshipEndingViewModel : BaseMailModel
{
}
}

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Models.Mail.FamiliesForEnterprise
{
public class FamiliesForEnterpriseSponsorshipRevertingViewModel : BaseMailModel
{
}
}

View File

@ -49,5 +49,13 @@ namespace Bit.Core.Services
Task SendProviderConfirmedEmailAsync(string providerName, string email);
Task SendProviderUserRemoved(string providerName, string email);
Task SendUpdatedTempPasswordEmailAsync(string email, string userName);
// TODO: Change signature to hold data needed for email
Task SendFamiliesForEnterpriseInviteRedeemableEmailAsync(string email, string organizationName, string token);
// NOTE: Not married to these next two names
Task SendFamiliesForEnterpriseInviteRedeemedToFamilyUserEmailAsync(string email);
Task SendFamiliesForEnterpriseInviteRedeemedToOrgUserEmailAsync(string email, string organizationName);
Task SendFamiliesForEnterpriseReconfirmationRequiredEmailAsync(string email);
Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email);
Task SendFamiliesForEnterpriseSponsorshipEndingEmailAsync(string email);
}
}

View File

@ -9,6 +9,7 @@ using System.Net;
using Bit.Core.Utilities;
using System.Linq;
using System.Reflection;
using Bit.Core.Models.Mail.FamiliesForEnterprise;
using Bit.Core.Models.Mail.Provider;
using Bit.Core.Models.Table.Provider;
using HandlebarsDotNet;
@ -755,5 +756,93 @@ namespace Bit.Core.Services
message.Category = "UpdatedTempPassword";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendFamiliesForEnterpriseInviteRedeemableEmailAsync(string email, string organizationName, string token)
{
// TODO: Complete emails
var message = CreateDefaultMessage("A Family Organization Invite Is Redeemable", email);
// NOTE: If somehow cloud vault changes this will need to change/be injected
var url = CoreHelpers.ExtendQuery(new Uri($"https://vault.bitwarden.com/#/sponsored/families-for-enterprise"),
new Dictionary<string, string>
{
["sponsorshipToken"] = token,
});
var model = new FamiliesForEnterpriseInviteRedeemableViewModel
{
Url = url.ToString(),
OrganizationName = organizationName,
};
await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseInviteRedeemable", model);
message.Category = "FamiliesForEnterpriseInviteRedeemable";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendFamiliesForEnterpriseInviteRedeemedToFamilyUserEmailAsync(string email)
{
// TODO: Complete emails
var message = CreateDefaultMessage("You Have Redeemed A Family Organization Sponsorship", email);
var model = new FamiliesForEnterpriseInviteRedeemedToFamilyUserViewModel
{
};
await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseInviteRedeemedToFamilyUser", model);
message.Category = "FamilyForEnterpriseInviteRedeemedToFamilyUser";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendFamiliesForEnterpriseInviteRedeemedToOrgUserEmailAsync(string email, string organizationName)
{
// TODO: Complete emails
var message = CreateDefaultMessage("A User Has Redeemeed Your Sponsorship", email);
var model = new FamiliesForEnterpriseInviteRedeemedToOrgUserViewModel
{
OrganizationName = organizationName,
};
await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseInviteRedeemedToOrgUser", model);
message.Category = "FamilyForEnterpriseInviteRedeemedToOrgUser";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendFamiliesForEnterpriseReconfirmationRequiredEmailAsync(string email)
{
// TODO: Complete emails
var message = CreateDefaultMessage("Your Sponsorship Requires Reconfirmation", email);
var model = new FamiliesForEnterpriseReconfirmationRequiredViewModel
{
};
await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseReconfirmationRequired", model);
message.Category = "FamiliesForEnterpriseReconfirmationRequired";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email)
{
// TODO: Complete emails
var message = CreateDefaultMessage("A Family Organization Sponsorship Is Reverting", email);
var model = new FamiliesForEnterpriseSponsorshipRevertingViewModel
{
};
await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseSponsorshipReverting", model);
message.Category = "FamiliesForEnterpriseSponsorshipReverting";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendFamiliesForEnterpriseSponsorshipEndingEmailAsync(string email)
{
// TODO: Complete emails
var message = CreateDefaultMessage("A Family Organization Sponsorship Is Ending", email);
var model = new FamiliesForEnterpriseSponsorshipEndingViewModel
{
};
await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseSponsorshipEnding", model);
message.Category = "FamiliesForEnterpriseSponsorshipEnding";
await _mailDeliveryService.SendEmailAsync(message);
}
}
}

View File

@ -200,5 +200,35 @@ namespace Bit.Core.Services
{
return Task.FromResult(0);
}
public Task SendFamiliesForEnterpriseInviteRedeemableEmailAsync(string email, string organizationName, string token)
{
return Task.FromResult(0);
}
public Task SendFamiliesForEnterpriseInviteRedeemedToFamilyUserEmailAsync(string email)
{
return Task.FromResult(0);
}
public Task SendFamiliesForEnterpriseInviteRedeemedToOrgUserEmailAsync(string email, string organizationName)
{
return Task.FromResult(0);
}
public Task SendFamiliesForEnterpriseReconfirmationRequiredEmailAsync(string email)
{
return Task.FromResult(0);
}
public Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email)
{
return Task.FromResult(0);
}
public Task SendFamiliesForEnterpriseSponsorshipEndingEmailAsync(string email)
{
return Task.FromResult(0);
}
}
}

View File

@ -1,6 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Bit.Core.Models.Business;
using Bit.Core.Models.Table;
using Bit.Core.Models.Table.Provider;
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
@ -27,6 +35,131 @@ namespace Bit.Core.Test.Services
);
}
[Fact(Skip = "Only for local development")]
public async Task SendAllEmails()
{
// This test is only opt in and is more for development purposes.
// This will send all emails to the test email address so that they can be viewed.
var namedParameters = new Dictionary<(string, Type), object>
{
// TODO: Swith to use env variable
{ ("email", typeof(string)), "test@bitwarden.com" },
{ ("user", typeof(User)), new User
{
Id = Guid.NewGuid(),
Email = "test@bitwarden.com",
}},
{ ("userId", typeof(Guid)), Guid.NewGuid() },
{ ("token", typeof(string)), "test_token" },
{ ("fromEmail", typeof(string)), "test@bitwarden.com" },
{ ("toEmail", typeof(string)), "test@bitwarden.com" },
{ ("newEmailAddress", typeof(string)), "test@bitwarden.com" },
{ ("hint", typeof(string)), "Test Hint" },
{ ("organizationName", typeof(string)), "Test Organization Name" },
{ ("orgUser", typeof(OrganizationUser)), new OrganizationUser
{
Id = Guid.NewGuid(),
Email = "test@bitwarden.com",
OrganizationId = Guid.NewGuid(),
}},
{ ("token", typeof(ExpiringToken)), new ExpiringToken("test_token", DateTime.UtcNow.AddDays(1))},
{ ("organization", typeof(Organization)), new Organization
{
Id = Guid.NewGuid(),
Name = "Test Organization Name",
Seats = 5
}},
{ ("initialSeatCount", typeof(int)), 5},
{ ("ownerEmails", typeof(IEnumerable<string>)), new [] { "test@bitwarden.com" }},
{ ("maxSeatCount", typeof(int)), 5 },
{ ("userIdentifier", typeof(string)), "test_user" },
{ ("adminEmails", typeof(IEnumerable<string>)), new [] { "test@bitwarden.com" }},
{ ("returnUrl", typeof(string)), "https://bitwarden.com/" },
{ ("amount", typeof(decimal)), 1.00M },
{ ("dueDate", typeof(DateTime)), DateTime.UtcNow.AddDays(1) },
{ ("items", typeof(List<string>)), new List<string> { "test@bitwarden.com" }},
{ ("mentionInvoices", typeof(bool)), true },
{ ("emails", typeof(IEnumerable<string>)), new [] { "test@bitwarden.com" }},
{ ("deviceType", typeof(string)), "Mobile" },
{ ("timestamp", typeof(DateTime)), DateTime.UtcNow.AddDays(1)},
{ ("ip", typeof(string)), "127.0.0.1" },
{ ("emergencyAccess", typeof(EmergencyAccess)), new EmergencyAccess
{
Id = Guid.NewGuid(),
Email = "test@bitwarden.com",
}},
{ ("granteeEmail", typeof(string)), "test@bitwarden.com" },
{ ("grantorName", typeof(string)), "Test User" },
{ ("initiatingName", typeof(string)), "Test" },
{ ("approvingName", typeof(string)), "Test Name" },
{ ("rejectingName", typeof(string)), "Test Name" },
{ ("provider", typeof(Provider)), new Provider
{
Id = Guid.NewGuid(),
}},
{ ("name", typeof(string)), "Test Name" },
{ ("ea", typeof(EmergencyAccess)), new EmergencyAccess
{
Id = Guid.NewGuid(),
Email = "test@bitwarden.com",
}},
{ ("userName", typeof(string)), "testUser" },
{ ("orgName", typeof(string)), "Test Org Name" },
{ ("providerName", typeof(string)), "testProvider" },
{ ("providerUser", typeof(ProviderUser)), new ProviderUser
{
ProviderId = Guid.NewGuid(),
Id = Guid.NewGuid(),
}},
};
var globalSettings = new GlobalSettings
{
Mail = new GlobalSettings.MailSettings
{
Smtp = new GlobalSettings.MailSettings.SmtpSettings
{
Host = "localhost",
TrustServer = true,
Port = 10250,
},
ReplyToEmail = "noreply@bitwarden.com",
},
SiteName = "Bitwarden",
};
var mailDeliveryService = new MailKitSmtpMailDeliveryService(globalSettings, Substitute.For<ILogger<MailKitSmtpMailDeliveryService>>());
var handlebarsService = new HandlebarsMailService(globalSettings, mailDeliveryService, new BlockingMailEnqueuingService());
var sendMethods = typeof(IMailService).GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.Name.StartsWith("Send") && m.Name != "SendEnqueuedMailMessageAsync");
foreach (var sendMethod in sendMethods)
{
await InvokeMethod(sendMethod);
}
async Task InvokeMethod(MethodInfo method)
{
var parameters = method.GetParameters();
var args = new object[parameters.Length];
for(var i = 0; i < parameters.Length; i++)
{
if (!namedParameters.TryGetValue((parameters[i].Name, parameters[i].ParameterType), out var value))
{
throw new InvalidOperationException($"Couldn't find a parameter for name '{parameters[i].Name}' and type '{parameters[i].ParameterType.FullName}'");
}
args[i] = value;
}
await (Task)method.Invoke(handlebarsService, args);
}
}
// Remove this test when we add actual tests. It only proves that
// we've properly constructed the system under test.
[Fact]