diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index c2d0471a6e..a183f69ec9 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -8,6 +8,17 @@ + + + + + + + + + + + diff --git a/src/Core/MailTemplates/Markdown/ChangeEmail.md b/src/Core/MailTemplates/Markdown/ChangeEmail.md new file mode 100644 index 0000000000..a9aaaa2b2a --- /dev/null +++ b/src/Core/MailTemplates/Markdown/ChangeEmail.md @@ -0,0 +1 @@ +To finalize changing your email address enter the following code in the pop-up window: {{token}} diff --git a/src/Core/MailTemplates/Markdown/ChangeEmailAlreadyExists.md b/src/Core/MailTemplates/Markdown/ChangeEmailAlreadyExists.md new file mode 100644 index 0000000000..e46ab95e43 --- /dev/null +++ b/src/Core/MailTemplates/Markdown/ChangeEmailAlreadyExists.md @@ -0,0 +1,3 @@ +A user ({{fromEmail}}) recently tried to change their account to use this email address ({{toEmail}}). An account already exists with this email ({{toEmail}}). + +If you did not try to change an email address, you can safely ignore this email. diff --git a/src/Core/MailTemplates/Markdown/MasterPasswordHint.md b/src/Core/MailTemplates/Markdown/MasterPasswordHint.md new file mode 100644 index 0000000000..271e1d1c38 --- /dev/null +++ b/src/Core/MailTemplates/Markdown/MasterPasswordHint.md @@ -0,0 +1,7 @@ +You (or someone) recently requested your master password hint. + +Your hint is: "{{hint}}" + +Login: <{{vaultUrl}}> + +If you did not request your master password hint you can safely ignore this email. diff --git a/src/Core/MailTemplates/Markdown/NoMasterPasswordHint.md b/src/Core/MailTemplates/Markdown/NoMasterPasswordHint.md new file mode 100644 index 0000000000..9ae9295463 --- /dev/null +++ b/src/Core/MailTemplates/Markdown/NoMasterPasswordHint.md @@ -0,0 +1,3 @@ +You (or someone) recently requested your master password hint. Unfortunately, your account does not have a master password hint. + +If you did not request your master password hint you can safely ignore this email. diff --git a/src/Core/MailTemplates/Markdown/OrganizationUserAccepted.md b/src/Core/MailTemplates/Markdown/OrganizationUserAccepted.md new file mode 100644 index 0000000000..8c6dd1bde0 --- /dev/null +++ b/src/Core/MailTemplates/Markdown/OrganizationUserAccepted.md @@ -0,0 +1,5 @@ +This email is to notify you that {{userEmail}} has accepted your invitation to join {{organizationName}}. + +To confirm this user, log into the bitwarden web vault, manage your organization "People" and confirm the user. + +If you do not wish to confirm this user, you can also remove them from the organization on the same page. diff --git a/src/Core/MailTemplates/Markdown/OrganizationUserConfirmed.md b/src/Core/MailTemplates/Markdown/OrganizationUserConfirmed.md new file mode 100644 index 0000000000..4ef73877ab --- /dev/null +++ b/src/Core/MailTemplates/Markdown/OrganizationUserConfirmed.md @@ -0,0 +1,3 @@ +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. diff --git a/src/Core/MailTemplates/Markdown/OrganizationUserInvited.md b/src/Core/MailTemplates/Markdown/OrganizationUserInvited.md new file mode 100644 index 0000000000..55400c4e36 --- /dev/null +++ b/src/Core/MailTemplates/Markdown/OrganizationUserInvited.md @@ -0,0 +1,6 @@ +You have been invited to join the {{organizationName}} organization. To accept this invite, click the +following link: + +<{{url}}> + +If you do not wish to join this organization, you can safely ignore this email. diff --git a/src/Core/MailTemplates/Markdown/TwoFactorEmail.md b/src/Core/MailTemplates/Markdown/TwoFactorEmail.md new file mode 100644 index 0000000000..5bd8d81e85 --- /dev/null +++ b/src/Core/MailTemplates/Markdown/TwoFactorEmail.md @@ -0,0 +1,3 @@ +Your two-step verification code is: {{token}} + +Use this code to complete logging in with bitwarden. diff --git a/src/Core/MailTemplates/Markdown/VerifyDelete.md b/src/Core/MailTemplates/Markdown/VerifyDelete.md new file mode 100644 index 0000000000..b77c8a3145 --- /dev/null +++ b/src/Core/MailTemplates/Markdown/VerifyDelete.md @@ -0,0 +1,3 @@ +Click the link below to delete your bitwarden account ({{email}}). If you did not request this email to delete your bitwarden account, you can safely ignore it. + +<{{url}}> diff --git a/src/Core/MailTemplates/Markdown/VerifyEmail.md b/src/Core/MailTemplates/Markdown/VerifyEmail.md new file mode 100644 index 0000000000..fa806b4c78 --- /dev/null +++ b/src/Core/MailTemplates/Markdown/VerifyEmail.md @@ -0,0 +1,3 @@ +Verify this email address for your bitwarden account by clicking the following link. If you did not request this email to verify a bitwarden account, you can safely ignore it. + +<{{url}}> diff --git a/src/Core/MailTemplates/Markdown/Welcome.md b/src/Core/MailTemplates/Markdown/Welcome.md new file mode 100644 index 0000000000..08901e76a8 --- /dev/null +++ b/src/Core/MailTemplates/Markdown/Welcome.md @@ -0,0 +1,40 @@ +Thank you for creating an account with bitwarden. You may now log in with your new account. + +Did you know that bitwarden is free to sync with all of your devices? Download bitwarden today on: + +Mobile +============ + +## iOS + + +## Android + + +Desktop +============ + +## Chrome Extension + + +## Firefox Extension + + +## Opera Extension + + +## Edge Extension + + +Web +============ + +You can also access your vault from any web-enabled device using our web vault at: <{{vaultUrl}}?utm_source=welcome_email&utm_medium=email> + +------------ + +If you have any questions or problems you can email us from our website at: + + +Thank you! +The bitwarden Team diff --git a/src/Core/Services/Implementations/MarkdownMailService.cs b/src/Core/Services/Implementations/MarkdownMailService.cs new file mode 100644 index 0000000000..d669b71ba8 --- /dev/null +++ b/src/Core/Services/Implementations/MarkdownMailService.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Table; +using Bit.Core.Models.Mail; +using System.IO; +using System.Net; +using System.Reflection; + +namespace Bit.Core.Services +{ + public class MarkdownMailService : IMailService + { + private const string Namespace = "Bit.Core.MailTemplates.Markdown"; + + private readonly GlobalSettings _globalSettings; + private readonly IMailDeliveryService _mailDeliveryService; + + public MarkdownMailService( + GlobalSettings globalSettings, + IMailDeliveryService mailDeliveryService) + { + _globalSettings = globalSettings; + _mailDeliveryService = mailDeliveryService; + } + + public async Task SendVerifyEmailEmailAsync(string email, Guid userId, string token) + { + var model = new Dictionary + { + ["url"] = string.Format("{0}/verify-email?userId={1}&token={2}", + _globalSettings.BaseServiceUri.VaultWithHash, userId, WebUtility.UrlEncode(token)) + }; + + var message = await CreateMessageAsync("Verify Your Email", email, "VerifyEmail", model); + message.MetaData.Add("SendGridBypassListManagement", true); + await _mailDeliveryService.SendEmailAsync(message); + } + + public async Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token) + { + var model = new Dictionary + { + ["url"] = string.Format("{0}/verify-recover-delete?userId={1}&token={2}&email={3}", + _globalSettings.BaseServiceUri.VaultWithHash, + userId, + WebUtility.UrlEncode(token), + WebUtility.UrlEncode(email)), + ["email"] = WebUtility.HtmlEncode(email) + }; + + var message = await CreateMessageAsync("Delete Your Account", email, "VerifyDelete", model); + message.MetaData.Add("SendGridBypassListManagement", true); + await _mailDeliveryService.SendEmailAsync(message); + } + + public async Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail) + { + var model = new Dictionary + { + ["fromEmail"] = WebUtility.HtmlEncode(fromEmail), + ["toEmail"] = WebUtility.HtmlEncode(toEmail), + }; + + var message = await CreateMessageAsync("Your Email Change", toEmail, "ChangeEmailAlreadyExists", model); + message.MetaData.Add("SendGridBypassListManagement", true); + await _mailDeliveryService.SendEmailAsync(message); + } + + public async Task SendChangeEmailEmailAsync(string newEmailAddress, string token) + { + var model = new Dictionary + { + ["token"] = token + }; + + var message = await CreateMessageAsync("Your Email Change", newEmailAddress, "ChangeEmail", model); + message.MetaData.Add("SendGridBypassListManagement", true); + await _mailDeliveryService.SendEmailAsync(message); + } + + public async Task SendTwoFactorEmailAsync(string email, string token) + { + var model = new Dictionary + { + ["token"] = token + }; + + var message = await CreateMessageAsync("Your Two-step Login Verification Code", email, "TwoFactorEmail", model); + message.MetaData.Add("SendGridBypassListManagement", true); + await _mailDeliveryService.SendEmailAsync(message); + } + + public async Task SendMasterPasswordHintEmailAsync(string email, string hint) + { + var model = new Dictionary + { + ["hint"] = WebUtility.HtmlEncode(hint), + ["vaultUrl"] = _globalSettings.BaseServiceUri.VaultWithHash + }; + + var message = await CreateMessageAsync("Your Master Password Hint", email, "MasterPasswordHint", model); + message.MetaData.Add("SendGridBypassListManagement", true); + await _mailDeliveryService.SendEmailAsync(message); + } + + public async Task SendNoMasterPasswordHintEmailAsync(string email) + { + var message = await CreateMessageAsync("Your Master Password Hint", email, "NoMasterPasswordHint", null); + message.MetaData.Add("SendGridBypassListManagement", true); + await _mailDeliveryService.SendEmailAsync(message); + } + + public async Task SendOrganizationAcceptedEmailAsync(string organizationName, string userEmail, + IEnumerable adminEmails) + { + var model = new Dictionary + { + ["userEmail"] = WebUtility.HtmlEncode(userEmail), + ["organizationName"] = WebUtility.HtmlEncode(organizationName) + }; + + var message = await CreateMessageAsync($"User {userEmail} Has Accepted Invite", adminEmails, + "OrganizationUserAccepted", model); + message.MetaData.Add("SendGridBypassListManagement", true); + await _mailDeliveryService.SendEmailAsync(message); + } + + public async Task SendOrganizationConfirmedEmailAsync(string organizationName, string email) + { + var model = new Dictionary + { + ["organizationName"] = WebUtility.HtmlEncode(organizationName) + }; + + var message = await CreateMessageAsync($"You Have Been Confirmed To {organizationName}", email, + "OrganizationUserConfirmed", model); + message.MetaData.Add("SendGridBypassListManagement", true); + await _mailDeliveryService.SendEmailAsync(message); + } + + public async Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, string token) + { + var model = new Dictionary + { + ["organizationName"] = WebUtility.HtmlEncode(organizationName), + ["url"] = string.Format("{0}/accept-organization?organizationId={1}&organizationUserId={2}" + + "&email={3}&organizationName={4}&token={5}", + _globalSettings.BaseServiceUri.VaultWithHash, + orgUser.OrganizationId, + orgUser.Id, + WebUtility.UrlEncode(orgUser.Email), + WebUtility.UrlEncode(organizationName), + WebUtility.UrlEncode(token)) + }; + + var message = await CreateMessageAsync($"Join {organizationName}", orgUser.Email, "OrganizationUserInvited", model); + message.MetaData.Add("SendGridBypassListManagement", true); + await _mailDeliveryService.SendEmailAsync(message); + } + + public async Task SendWelcomeEmailAsync(User user) + { + var model = new Dictionary + { + ["vaultUrl"] = _globalSettings.BaseServiceUri.VaultWithHash + }; + + var message = await CreateMessageAsync("Welcome", user.Email, "Welcome", model); + await _mailDeliveryService.SendEmailAsync(message); + } + + private async Task CreateMessageAsync(string subject, string toEmail, string fileName, + Dictionary model) + { + return await CreateMessageAsync(subject, new List { toEmail }, fileName, model); + } + + private async Task CreateMessageAsync(string subject, IEnumerable toEmails, string fileName, + Dictionary model) + { + var message = new MailMessage + { + ToEmails = toEmails, + Subject = subject, + MetaData = new Dictionary() + }; + + var assembly = typeof(MarkdownMailService).GetTypeInfo().Assembly; + using(var s = assembly.GetManifestResourceStream($"{Namespace}.{fileName}.md")) + using(var sr = new StreamReader(s)) + { + var markdown = await sr.ReadToEndAsync(); + + if(model != null) + { + foreach(var prop in model) + { + markdown = markdown.Replace($"{{{{{prop.Key}}}}}", prop.Value); + } + } + + message.HtmlContent = CommonMark.CommonMarkConverter.Convert(markdown); + message.TextContent = markdown; + } + + return message; + } + } +}