Files
server/src/Core/Services/Implementations/HandlebarsMailService.cs
Matt Gibson c54c39b28c Feature/self hosted families for enterprise (#1991)
* Families for enterprise/split up organization sponsorship service (#1829)

* Split OrganizationSponsorshipService into commands

* Use tokenable for token validation

* Use interfaces to set up for DI

* Use commands over services

* Move service tests to command tests

* Value types can't be null

* Run dotnet format

* Update src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommand.cs

Co-authored-by: Justin Baur <admin@justinbaur.com>

* Fix controller tests

Co-authored-by: Justin Baur <admin@justinbaur.com>

* Families for enterprise/split up organization sponsorship service (#1875)

* Split OrganizationSponsorshipService into commands

* Use tokenable for token validation

* Use interfaces to set up for DI

* Use commands over services

* Move service tests to command tests

* Value types can't be null

* Run dotnet format

* Update src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommand.cs

Co-authored-by: Justin Baur <admin@justinbaur.com>

* Fix controller tests

* Split create and send sponsorships

* Split up create sponsorship

* Add self hosted commands to dependency injection

* Add field to store cloud billing sync key on self host instances

* Fix typo

* Fix data protector purpose of sponsorship offers

* Split cloud and selfhosted sponsorship offer tokenable

* Generate offer from self hosted with all necessary auth data

* Add Required properties to constructor

* Split up cancel sponsorship command

* Split revoke sponsorship command between cloud and self hosted

* Fix/f4e multiple sponsorships (#1838)

* Use sponosorship from validate to redeem

* Update tests

* Format

* Remove sponsorship service

* Run dotnet format

* Fix self hosted only controller attribute

* Clean up file structure and fixes

* Remove unneeded tokenables

* Remove obsolete commands

* Do not require file/class prefix if unnecessary

* Update Organizaiton sprocs

* Remove unnecessary models

* Fix tests

* Generalize LicenseService path calculation

Use async file read and deserialization

* Use interfaces for testability

* Remove unused usings

* Correct test direction

* Test license reading

* remove unused usings

* Format

Co-authored-by: Justin Baur <admin@justinbaur.com>

* Improve DataProtectorTokenFactory test coverage (#1884)

* Add encstring to server

* Test factory

Co-authored-by: Carlos Muentes <cmuentes@bitwarden.com>

* Format

* Remove SymmetricKeyProtectedString

Not needed

* Set ForcInvalid

Co-authored-by: Carlos Muentes <cmuentes@bitwarden.com>

* Feature/self f4e/api keys (#1896)

* Add in ApiKey

* Work on API Key table

* Work on apikey table

* Fix response model

* Work on information for UI

* Work on last sync date

* Work on sync status

* Work on auth

* Work on tokenable

* Work on merge

* Add custom requirement

* Add policy

* Run formatting

* Work on EF Migrations

* Work on OrganizationConnection

* Work on database

* Work on additional database table

* Run formatting

* Small fixes

* More cleanup

* Cleanup

* Add RevisionDate

* Add GO

* Finish Sql project

* Add newlines

* Fix stored proc file

* Fix sqlproj

* Add newlines

* Fix table

* Add navigation property

* Delete Connections when organization is deleted

* Add connection validation

* Start adding ID column

* Work on ID column

* Work on SQL migration

* Work on migrations

* Run formatting

* Fix test build

* Fix sprocs

* Work on migrations

* Fix Create table

* Fix sproc

* Add prints to migration

* Add default value

* Update EF migrations

* Formatting

* Add to integration tests

* Minor fixes

* Formatting

* Cleanup

* Address PR feedback

* Address more PR feedback

* Fix formatting

* Fix formatting

* Fix

* Address PR feedback

* Remove accidential change

* Fix SQL build

* Run formatting

* Address PR feedback

* Add sync data to OrganizationUserOrgDetails

* Add comments

* Remove OrganizationConnectionService interface

* Remove unused using

* Address PR feedback

* Formatting

* Minor fix

* Feature/self f4e/update db (#1930)

* Fix migration

* Fix TimesRenewed

* Add comments

* Make two properties non-nullable

* Remove need for SponsoredOrg on SH (#1934)

* Remove need for SponsoredOrg on SH

* Add Family prefix

* Add check for enterprise org on BillingSync key (#1936)

* [PS-10] Feature/sponsorships removed at end of term (#1938)

* Rename commands to min unique names

* Inject revoke command based on self hosting

* WIP: Remove/Revoke marks to delete

* Complete WIP

* Improve remove/revoke tests

* PR review

* Fail validation if sponsorship has failed to sync for 6 months

* Feature/do not accept old self host sponsorships (#1939)

* Do not accept >6mo old self-hosted sponsorships

* Give disabled grace period of 3 months

* Fix issues of Sql.proj differing from migration outcome (#1942)

* Fix issues of Sql.proj differing from migration outcome

* Yoink int tests

* Add missing assert helpers

* Feature/org sponsorship sync (#1922)

* Self-hosted side sync first pass

TODO:
* flush out org sponsorship model
* implement cloud side
* process cloud-side response and update self-hosted records

* sync scaffolding second pass

* remove list of Org User ids from sync and begin work on SelfHostedRevokeSponsorship

* allow authenticated http calls from server to return a result

* update models

* add logic for sync and change offer email template

* add billing sync key and hide CreateSponsorship without user

* fix tests

* add job scheduling

* add authorize attributes to endpoints

* separate models into data/model and request/response

* batch sync more, add EnableCloudCommunication for testing

* send emails in bulk

* make userId and sponsorshipType non nullable

* batch more on self hosted side of sync

* remove TODOs and formatting

* changed logic of cloud sync

* let BaseIdentityClientService handle all logging

* call sync from scheduled job on self host

* create bulk db operations for OrganizationSponsorships

* remove SponsoredOrgId from sync, return default from server http call

* validate BillingSyncKey during sync

revert changes to CreateSponsorshipCommand

* revert changes to ICreateSponsorshipCommand

* add some tests

* add DeleteExpiredSponsorshipsJob

* add cloud sync test

* remove extra method

* formatting

* prevent new sponsorships from disabled orgs

* update packages

* - pulled out send sponsorship command dependency from sync on cloud
- don't throw error when sponsorships are empty
- formatting

* formatting models

* more formatting

* remove licensingService dependency from selfhosted sync

* use installation urls and formatting

* create constructor for RequestModel and formatting

* add date parameter to OrganizationSponsorship_DeleteExpired

* add new migration

* formatting

* rename OrganizationCreateSponsorshipRequestModel to OrganizationSponsorshipCreateRequestModel

* prevent whole sync from failing if one sponsorship type is unsupported

* deserialize config and billingsynckey from org connection

* alter log message when sync disabled

* Add grace period to disabled orgs

* return early on self hosted if there are no sponsorships in database

* rename BillingSyncConfig

* send sponsorship offers from controller

* allow config to be a null object

* better exception handling in sync scheduler

* add ef migrations

* formatting

* fix tests

* fix validate test

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>

* Fix OrganizationApiKey issues (#1941)

Co-authored-by: Justin Baur <admin@justinbaur.com>

* Feature/org sponsorship self hosted tests (#1947)

* Self-hosted side sync first pass

TODO:
* flush out org sponsorship model
* implement cloud side
* process cloud-side response and update self-hosted records

* sync scaffolding second pass

* remove list of Org User ids from sync and begin work on SelfHostedRevokeSponsorship

* allow authenticated http calls from server to return a result

* update models

* add logic for sync and change offer email template

* add billing sync key and hide CreateSponsorship without user

* fix tests

* add job scheduling

* add authorize attributes to endpoints

* separate models into data/model and request/response

* batch sync more, add EnableCloudCommunication for testing

* send emails in bulk

* make userId and sponsorshipType non nullable

* batch more on self hosted side of sync

* remove TODOs and formatting

* changed logic of cloud sync

* let BaseIdentityClientService handle all logging

* call sync from scheduled job on self host

* create bulk db operations for OrganizationSponsorships

* remove SponsoredOrgId from sync, return default from server http call

* validate BillingSyncKey during sync

revert changes to CreateSponsorshipCommand

* revert changes to ICreateSponsorshipCommand

* add some tests

* add DeleteExpiredSponsorshipsJob

* add cloud sync test

* remove extra method

* formatting

* prevent new sponsorships from disabled orgs

* update packages

* - pulled out send sponsorship command dependency from sync on cloud
- don't throw error when sponsorships are empty
- formatting

* formatting models

* more formatting

* remove licensingService dependency from selfhosted sync

* use installation urls and formatting

* create constructor for RequestModel and formatting

* add date parameter to OrganizationSponsorship_DeleteExpired

* add new migration

* formatting

* rename OrganizationCreateSponsorshipRequestModel to OrganizationSponsorshipCreateRequestModel

* prevent whole sync from failing if one sponsorship type is unsupported

* deserialize config and billingsynckey from org connection

* add mockHttp nuget package and use httpclientfactory

* fix current tests

* WIP of creating tests

* WIP of new self hosted tests

* WIP self hosted tests

* finish self hosted tests

* formatting

* format of interface

* remove extra config file

* added newlines

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>

* Fix Organization_DeleteById (#1950)

* Fix Organization_Delete

* Fix L

* [PS-4] block enterprise user from sponsoring itself (#1943)

* [PS-248] Feature/add connections enabled endpoint (#1953)

* Move Organization models to sub namespaces

* Add Organization Connection api endpoints

* Get all connections rather than just enabled ones

* Add missing services to DI

* pluralize private api endpoints

* Add type protection to org connection request/response

* Fix route

* Use nullable Id to signify no connection

* Test Get Connections enabled

* Fix data discoverer

* Also drop this sproc for rerunning

* Id is the OUTPUT of create sprocs

* Fix connection config parsing

* Linter fixes

* update sqlproj file name

* Use param xdocs on methods

* Simplify controller path attribute

* Use JsonDocument to avoid escaped json in our response/request strings

* Fix JsonDoc tests

* Linter fixes

* Fix ApiKey Command and add tests (#1949)

* Fix ApiKey command

* Formatting

* Fix test failures introduced in #1943 (#1957)

* Remove "Did you know?" copy from emails. (#1962)

* Remove "Did you know"

* Remove jsonIf helper

* Feature/fix send single sponsorship offer email (#1956)

* Fix sponsorship offer email

* Do not sanitize org name

* PR feedback

* Feature/f4e sync event [PS-75] (#1963)

* Create sponsorship sync event type

* Add InstallationId to Event model

* Add combinatorics-based test case generators

* Log sponsorships sync event on sync

* Linter and test fixes

* Fix failing test

* Migrate sprocs and view

* Remove unused `using`s

* [PS-190] Add manual sync trigger in self hosted (#1955)

* WIP add button to admin project for billing sync

* add connection table to view page

* minor fixes for self hosted side of sync

* fixes number of bugs for cloud side of sync

* deserialize before returning for some reason

* add json attributes to return models

* list of sponsorships parameter is immutable, add secondary list

* change sproc name

* add error handling

* Fix tests

* modify call to connection

* Update src/Admin/Controllers/OrganizationsController.cs

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>

* undo change to sproc name

* simplify logic

* Update src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommand.cs

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>

* register services despite if self hosted or cloud

* remove json properties

* revert merge conflict

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>

* Update OrganizationSponsorship valid until when updating org expirati… (#1966)

* Update OrganizationSponsorship valid until when updating org expiration date

* Linter fixes

* [PS-7] change revert email copy and add ValidUntil to sponsorship (#1965)

* change revert email copy and add ValidUntil to sponsorship

* add 15 days if no ValidUntil

* Chore/merge/self hosted families for enterprise (#1972)

* Log swallowed HttpRequestExceptions (#1866)

Co-authored-by: Hinton <oscar@oscarhinton.com>

* Allow for utilization of  readonly db connection (#1937)

* Bump the pin of the download-artifacts action to bypass the broken GitHub api (#1952)

* Bumped version to 1.48.0 (#1958)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* [EC-160] Give Provider Users access to all org ciphers and collections (#1959)

* Bumped version to 1.48.1 (#1961)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Avoid sending "user need confirmation" emails when there are no org admins (#1960)

* Remove noncompliant users for new policies (#1951)

* [PS-284] Allow installation clients to not need a user. (#1968)

* Allow installation clients to not need a user.

* Run formatting

Co-authored-by: Andrei <30410186+Manolachi@users.noreply.github.com>
Co-authored-by: Hinton <oscar@oscarhinton.com>
Co-authored-by: sneakernuts <671942+sneakernuts@users.noreply.github.com>
Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Justin Baur <136baur@gmail.com>

* Fix/license file not found (#1974)

* Handle null license

* Throw hint message if license is not found by the admin project.

* Use CloudOrganizationId from Connection config

* Change test to support change

* Fix test

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>

* Feature/f4e selfhosted rename migration to .sql (#1971)

* rename migration to .sql

* format

* Add unit tests to self host F4E (#1975)

* Work on tests

* Added more tests

* Run linting

* Address PR feedback

* Fix AssertRecent

* Linting

* Fixed empty tests

* Fix/misc self hosted f4e (#1973)

* Allow setting of ApiUri

* Return updates sponsorshipsData objects

* Bind arguments by name

* Greedy load sponsorships to email.

When upsert was called, it creates Ids on _all_ records, which meant
that the lazy-evaluation from this call always returned an empty list.

* add scope for sync command DI in job. simplify error logic

* update the sync job to get CloudOrgId from the BillingSyncKey

Co-authored-by: Jacob Fink <jfink@bitwarden.com>

* Chore/merge/self hosted families for enterprise (#1987)

* Log swallowed HttpRequestExceptions (#1866)

Co-authored-by: Hinton <oscar@oscarhinton.com>

* Allow for utilization of  readonly db connection (#1937)

* Bump the pin of the download-artifacts action to bypass the broken GitHub api (#1952)

* Bumped version to 1.48.0 (#1958)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* [EC-160] Give Provider Users access to all org ciphers and collections (#1959)

* Bumped version to 1.48.1 (#1961)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Avoid sending "user need confirmation" emails when there are no org admins (#1960)

* Remove noncompliant users for new policies (#1951)

* [PS-284] Allow installation clients to not need a user. (#1968)

* Allow installation clients to not need a user.

* Run formatting

* Use accept flow for sponsorship offers (#1964)

* PS-82 check send 2FA email for new devices on TwoFactorController send-email-login (#1977)

* [Bug] Skip WebAuthn 2fa event logs during login flow (#1978)

* [Bug] Supress WebAuthn 2fa event logs during login process

* Formatting

* Simplified method call with new paramter input

* Update RealIps Description (#1980)

Describe the syntax of the real_ips configuration key with an example, to prevent type errors in the `setup` container when parsing `config.yml`

* add proper URI validation to duo host (#1984)

* captcha scores (#1967)

* captcha scores

* some api fixes

* check bot on captcha attribute

* Update src/Core/Services/Implementations/HCaptchaValidationService.cs

Co-authored-by: e271828- <e271828-@users.noreply.github.com>

Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
Co-authored-by: e271828- <e271828-@users.noreply.github.com>

* ensure no path specific in duo host (#1985)

Co-authored-by: Andrei <30410186+Manolachi@users.noreply.github.com>
Co-authored-by: Hinton <oscar@oscarhinton.com>
Co-authored-by: sneakernuts <671942+sneakernuts@users.noreply.github.com>
Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Justin Baur <136baur@gmail.com>
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Co-authored-by: Jordan Cooks <notnamed@users.noreply.github.com>
Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
Co-authored-by: e271828- <e271828-@users.noreply.github.com>

* Address feedback (#1990)

Co-authored-by: Justin Baur <admin@justinbaur.com>
Co-authored-by: Carlos Muentes <cmuentes@bitwarden.com>
Co-authored-by: Jake Fink <jfink@bitwarden.com>
Co-authored-by: Justin Baur <136baur@gmail.com>
Co-authored-by: Andrei <30410186+Manolachi@users.noreply.github.com>
Co-authored-by: Hinton <oscar@oscarhinton.com>
Co-authored-by: sneakernuts <671942+sneakernuts@users.noreply.github.com>
Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Co-authored-by: Jordan Cooks <notnamed@users.noreply.github.com>
Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
Co-authored-by: e271828- <e271828-@users.noreply.github.com>
2022-05-10 16:12:09 -05:00

891 lines
41 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text.Json;
using System.Threading.Tasks;
using Bit.Core.Entities;
using Bit.Core.Entities.Provider;
using Bit.Core.Models.Business;
using Bit.Core.Models.Mail;
using Bit.Core.Models.Mail.FamiliesForEnterprise;
using Bit.Core.Models.Mail.Provider;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using HandlebarsDotNet;
namespace Bit.Core.Services
{
public class HandlebarsMailService : IMailService
{
private const string Namespace = "Bit.Core.MailTemplates.Handlebars";
private readonly GlobalSettings _globalSettings;
private readonly IMailDeliveryService _mailDeliveryService;
private readonly IMailEnqueuingService _mailEnqueuingService;
private readonly Dictionary<string, Func<object, string>> _templateCache =
new Dictionary<string, Func<object, string>>();
private bool _registeredHelpersAndPartials = false;
public HandlebarsMailService(
GlobalSettings globalSettings,
IMailDeliveryService mailDeliveryService,
IMailEnqueuingService mailEnqueuingService)
{
_globalSettings = globalSettings;
_mailDeliveryService = mailDeliveryService;
_mailEnqueuingService = mailEnqueuingService;
}
public async Task SendVerifyEmailEmailAsync(string email, Guid userId, string token)
{
var message = CreateDefaultMessage("Verify Your Email", email);
var model = new VerifyEmailModel
{
Token = WebUtility.UrlEncode(token),
UserId = userId,
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "VerifyEmail", model);
message.MetaData.Add("SendGridBypassListManagement", true);
message.Category = "VerifyEmail";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token)
{
var message = CreateDefaultMessage("Delete Your Account", email);
var model = new VerifyDeleteModel
{
Token = WebUtility.UrlEncode(token),
UserId = userId,
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
Email = email,
EmailEncoded = WebUtility.UrlEncode(email)
};
await AddMessageContentAsync(message, "VerifyDelete", model);
message.MetaData.Add("SendGridBypassListManagement", true);
message.Category = "VerifyDelete";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail)
{
var message = CreateDefaultMessage("Your Email Change", toEmail);
var model = new ChangeEmailExistsViewModel
{
FromEmail = fromEmail,
ToEmail = toEmail,
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "ChangeEmailAlreadyExists", model);
message.Category = "ChangeEmailAlreadyExists";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendChangeEmailEmailAsync(string newEmailAddress, string token)
{
var message = CreateDefaultMessage("Your Email Change", newEmailAddress);
var model = new EmailTokenViewModel
{
Token = token,
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "ChangeEmail", model);
message.MetaData.Add("SendGridBypassListManagement", true);
message.Category = "ChangeEmail";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendTwoFactorEmailAsync(string email, string token)
{
var message = CreateDefaultMessage("Your Two-step Login Verification Code", email);
var model = new EmailTokenViewModel
{
Token = token,
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "TwoFactorEmail", model);
message.MetaData.Add("SendGridBypassListManagement", true);
message.Category = "TwoFactorEmail";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendNewDeviceLoginTwoFactorEmailAsync(string email, string token)
{
var message = CreateDefaultMessage("New Device Login Verification Code", email);
var model = new EmailTokenViewModel
{
Token = token,
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "NewDeviceLoginTwoFactorEmail", model);
message.MetaData.Add("SendGridBypassListManagement", true);
message.Category = "TwoFactorEmail";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendMasterPasswordHintEmailAsync(string email, string hint)
{
var message = CreateDefaultMessage("Your Master Password Hint", email);
var model = new MasterPasswordHintViewModel
{
Hint = CoreHelpers.SanitizeForEmail(hint),
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "MasterPasswordHint", model);
message.Category = "MasterPasswordHint";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendNoMasterPasswordHintEmailAsync(string email)
{
var message = CreateDefaultMessage("Your Master Password Hint", email);
var model = new BaseMailModel
{
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "NoMasterPasswordHint", model);
message.Category = "NoMasterPasswordHint";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable<string> ownerEmails)
{
var message = CreateDefaultMessage($"{organization.Name} Seat Count Has Increased", ownerEmails);
var model = new OrganizationSeatsAutoscaledViewModel
{
OrganizationId = organization.Id,
InitialSeatCount = initialSeatCount,
CurrentSeatCount = organization.Seats.Value,
};
await AddMessageContentAsync(message, "OrganizationSeatsAutoscaled", model);
message.Category = "OrganizationSeatsAutoscaled";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendOrganizationMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails)
{
var message = CreateDefaultMessage($"{organization.Name} Seat Limit Reached", ownerEmails);
var model = new OrganizationSeatsMaxReachedViewModel
{
OrganizationId = organization.Id,
MaxSeatCount = maxSeatCount,
};
await AddMessageContentAsync(message, "OrganizationSeatsMaxReached", model);
message.Category = "OrganizationSeatsMaxReached";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier,
IEnumerable<string> adminEmails)
{
var message = CreateDefaultMessage($"Action Required: {userIdentifier} Needs to Be Confirmed", adminEmails);
var model = new OrganizationUserAcceptedViewModel
{
OrganizationId = organization.Id,
OrganizationName = CoreHelpers.SanitizeForEmail(organization.Name, false),
UserIdentifier = userIdentifier,
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "OrganizationUserAccepted", model);
message.Category = "OrganizationUserAccepted";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendOrganizationConfirmedEmailAsync(string organizationName, string email)
{
var message = CreateDefaultMessage($"You Have Been Confirmed To {organizationName}", email);
var model = new OrganizationUserConfirmedViewModel
{
OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false),
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "OrganizationUserConfirmed", model);
message.Category = "OrganizationUserConfirmed";
await _mailDeliveryService.SendEmailAsync(message);
}
public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token) =>
BulkSendOrganizationInviteEmailAsync(organizationName, new[] { (orgUser, token) });
public async Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites)
{
MailQueueMessage CreateMessage(string email, object model)
{
var message = CreateDefaultMessage($"Join {organizationName}", email);
return new MailQueueMessage(message, "OrganizationUserInvited", model);
}
var messageModels = invites.Select(invite => CreateMessage(invite.orgUser.Email,
new OrganizationUserInvitedViewModel
{
OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false),
Email = WebUtility.UrlEncode(invite.orgUser.Email),
OrganizationId = invite.orgUser.OrganizationId.ToString(),
OrganizationUserId = invite.orgUser.Id.ToString(),
Token = WebUtility.UrlEncode(invite.token.Token),
ExpirationDate = $"{invite.token.ExpirationDate.ToLongDateString()} {invite.token.ExpirationDate.ToShortTimeString()} UTC",
OrganizationNameUrlEncoded = WebUtility.UrlEncode(organizationName),
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
}
));
await EnqueueMailAsync(messageModels);
}
public async Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email)
{
var message = CreateDefaultMessage($"You have been removed from {organizationName}", email);
var model = new OrganizationUserRemovedForPolicyTwoStepViewModel
{
OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false),
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "OrganizationUserRemovedForPolicyTwoStep", model);
message.Category = "OrganizationUserRemovedForPolicyTwoStep";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendWelcomeEmailAsync(User user)
{
var message = CreateDefaultMessage("Welcome to Bitwarden!", user.Email);
var model = new BaseMailModel
{
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "Welcome", model);
message.Category = "Welcome";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendPasswordlessSignInAsync(string returnUrl, string token, string email)
{
var message = CreateDefaultMessage("[Admin] Continue Logging In", email);
var url = CoreHelpers.ExtendQuery(new Uri($"{_globalSettings.BaseServiceUri.Admin}/login/confirm"),
new Dictionary<string, string>
{
["returnUrl"] = returnUrl,
["email"] = email,
["token"] = token,
});
var model = new PasswordlessSignInModel
{
Url = url.ToString()
};
await AddMessageContentAsync(message, "PasswordlessSignIn", model);
message.Category = "PasswordlessSignIn";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendInvoiceUpcomingAsync(string email, decimal amount, DateTime dueDate,
List<string> items, bool mentionInvoices)
{
var message = CreateDefaultMessage("Your Subscription Will Renew Soon", email);
var model = new InvoiceUpcomingViewModel
{
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
AmountDue = amount,
DueDate = dueDate,
Items = items,
MentionInvoices = mentionInvoices
};
await AddMessageContentAsync(message, "InvoiceUpcoming", model);
message.Category = "InvoiceUpcoming";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendPaymentFailedAsync(string email, decimal amount, bool mentionInvoices)
{
var message = CreateDefaultMessage("Payment Failed", email);
var model = new PaymentFailedViewModel
{
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
Amount = amount,
MentionInvoices = mentionInvoices
};
await AddMessageContentAsync(message, "PaymentFailed", model);
message.Category = "PaymentFailed";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendAddedCreditAsync(string email, decimal amount)
{
var message = CreateDefaultMessage("Account Credit Payment Processed", email);
var model = new AddedCreditViewModel
{
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
Amount = amount
};
await AddMessageContentAsync(message, "AddedCredit", model);
message.Category = "AddedCredit";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendLicenseExpiredAsync(IEnumerable<string> emails, string organizationName = null)
{
var message = CreateDefaultMessage("License Expired", emails);
var model = new LicenseExpiredViewModel
{
OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false),
};
await AddMessageContentAsync(message, "LicenseExpired", model);
message.Category = "LicenseExpired";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendNewDeviceLoggedInEmail(string email, string deviceType, DateTime timestamp, string ip)
{
var message = CreateDefaultMessage($"New Device Logged In From {deviceType}", email);
var model = new NewDeviceLoggedInModel
{
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
DeviceType = deviceType,
TheDate = timestamp.ToLongDateString(),
TheTime = timestamp.ToShortTimeString(),
TimeZone = "UTC",
IpAddress = ip
};
await AddMessageContentAsync(message, "NewDeviceLoggedIn", model);
message.Category = "NewDeviceLoggedIn";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendRecoverTwoFactorEmail(string email, DateTime timestamp, string ip)
{
var message = CreateDefaultMessage($"Recover 2FA From {ip}", email);
var model = new RecoverTwoFactorModel
{
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
TheDate = timestamp.ToLongDateString(),
TheTime = timestamp.ToShortTimeString(),
TimeZone = "UTC",
IpAddress = ip
};
await AddMessageContentAsync(message, "RecoverTwoFactor", model);
message.Category = "RecoverTwoFactor";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(string organizationName, string email)
{
var message = CreateDefaultMessage($"You have been removed from {organizationName}", email);
var model = new OrganizationUserRemovedForPolicySingleOrgViewModel
{
OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false),
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "OrganizationUserRemovedForPolicySingleOrg", model);
message.Category = "OrganizationUserRemovedForPolicySingleOrg";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendEnqueuedMailMessageAsync(IMailQueueMessage queueMessage)
{
var message = CreateDefaultMessage(queueMessage.Subject, queueMessage.ToEmails);
message.BccEmails = queueMessage.BccEmails;
message.Category = queueMessage.Category;
await AddMessageContentAsync(message, queueMessage.TemplateName, queueMessage.Model);
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendAdminResetPasswordEmailAsync(string email, string userName, string orgName)
{
var message = CreateDefaultMessage("Master Password Has Been Changed", email);
var model = new AdminResetPasswordViewModel()
{
UserName = CoreHelpers.SanitizeForEmail(userName),
OrgName = CoreHelpers.SanitizeForEmail(orgName),
};
await AddMessageContentAsync(message, "AdminResetPassword", model);
message.Category = "AdminResetPassword";
await _mailDeliveryService.SendEmailAsync(message);
}
private Task EnqueueMailAsync(IMailQueueMessage queueMessage) =>
_mailEnqueuingService.EnqueueAsync(queueMessage, SendEnqueuedMailMessageAsync);
private Task EnqueueMailAsync(IEnumerable<IMailQueueMessage> queueMessages) =>
_mailEnqueuingService.EnqueueManyAsync(queueMessages, SendEnqueuedMailMessageAsync);
private MailMessage CreateDefaultMessage(string subject, string toEmail)
{
return CreateDefaultMessage(subject, new List<string> { toEmail });
}
private MailMessage CreateDefaultMessage(string subject, IEnumerable<string> toEmails)
{
return new MailMessage
{
ToEmails = toEmails,
Subject = subject,
MetaData = new Dictionary<string, object>()
};
}
private async Task AddMessageContentAsync<T>(MailMessage message, string templateName, T model)
{
message.HtmlContent = await RenderAsync($"{templateName}.html", model);
message.TextContent = await RenderAsync($"{templateName}.text", model);
}
private async Task<string> RenderAsync<T>(string templateName, T model)
{
await RegisterHelpersAndPartialsAsync();
if (!_templateCache.TryGetValue(templateName, out var template))
{
var source = await ReadSourceAsync(templateName);
if (source != null)
{
template = Handlebars.Compile(source);
_templateCache.Add(templateName, template);
}
}
return template != null ? template(model) : null;
}
private async Task<string> ReadSourceAsync(string templateName)
{
var assembly = typeof(HandlebarsMailService).GetTypeInfo().Assembly;
var fullTemplateName = $"{Namespace}.{templateName}.hbs";
if (!assembly.GetManifestResourceNames().Any(f => f == fullTemplateName))
{
return null;
}
using (var s = assembly.GetManifestResourceStream(fullTemplateName))
using (var sr = new StreamReader(s))
{
return await sr.ReadToEndAsync();
}
}
private async Task RegisterHelpersAndPartialsAsync()
{
if (_registeredHelpersAndPartials)
{
return;
}
_registeredHelpersAndPartials = true;
var basicHtmlLayoutSource = await ReadSourceAsync("Layouts.Basic.html");
Handlebars.RegisterTemplate("BasicHtmlLayout", basicHtmlLayoutSource);
var basicTextLayoutSource = await ReadSourceAsync("Layouts.Basic.text");
Handlebars.RegisterTemplate("BasicTextLayout", basicTextLayoutSource);
var fullHtmlLayoutSource = await ReadSourceAsync("Layouts.Full.html");
Handlebars.RegisterTemplate("FullHtmlLayout", fullHtmlLayoutSource);
var fullTextLayoutSource = await ReadSourceAsync("Layouts.Full.text");
Handlebars.RegisterTemplate("FullTextLayout", fullTextLayoutSource);
Handlebars.RegisterHelper("date", (writer, context, parameters) =>
{
if (parameters.Length == 0 || !(parameters[0] is DateTime))
{
writer.WriteSafeString(string.Empty);
return;
}
if (parameters.Length > 0 && parameters[1] is string)
{
writer.WriteSafeString(((DateTime)parameters[0]).ToString(parameters[1].ToString()));
}
else
{
writer.WriteSafeString(((DateTime)parameters[0]).ToString());
}
});
Handlebars.RegisterHelper("usd", (writer, context, parameters) =>
{
if (parameters.Length == 0 || !(parameters[0] is decimal))
{
writer.WriteSafeString(string.Empty);
return;
}
writer.WriteSafeString(((decimal)parameters[0]).ToString("C"));
});
Handlebars.RegisterHelper("link", (writer, context, parameters) =>
{
if (parameters.Length == 0)
{
writer.WriteSafeString(string.Empty);
return;
}
var text = parameters[0].ToString();
var href = text;
var clickTrackingOff = false;
if (parameters.Length == 2)
{
if (parameters[1] is string)
{
var p1 = parameters[1].ToString();
if (p1 == "true" || p1 == "false")
{
clickTrackingOff = p1 == "true";
}
else
{
href = p1;
}
}
else if (parameters[1] is bool)
{
clickTrackingOff = (bool)parameters[1];
}
}
else if (parameters.Length > 2)
{
if (parameters[1] is string)
{
href = parameters[1].ToString();
}
if (parameters[2] is string)
{
var p2 = parameters[2].ToString();
if (p2 == "true" || p2 == "false")
{
clickTrackingOff = p2 == "true";
}
}
else if (parameters[2] is bool)
{
clickTrackingOff = (bool)parameters[2];
}
}
var clickTrackingText = (clickTrackingOff ? "clicktracking=off" : string.Empty);
writer.WriteSafeString($"<a href=\"{href}\" target=\"_blank\" {clickTrackingText}>{text}</a>");
});
}
public async Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token)
{
var message = CreateDefaultMessage($"Emergency Access Contact Invite", emergencyAccess.Email);
var model = new EmergencyAccessInvitedViewModel
{
Name = CoreHelpers.SanitizeForEmail(name),
Email = WebUtility.UrlEncode(emergencyAccess.Email),
Id = emergencyAccess.Id.ToString(),
Token = WebUtility.UrlEncode(token),
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "EmergencyAccessInvited", model);
message.Category = "EmergencyAccessInvited";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendEmergencyAccessAcceptedEmailAsync(string granteeEmail, string email)
{
var message = CreateDefaultMessage($"Accepted Emergency Access", email);
var model = new EmergencyAccessAcceptedViewModel
{
GranteeEmail = CoreHelpers.SanitizeForEmail(granteeEmail),
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "EmergencyAccessAccepted", model);
message.Category = "EmergencyAccessAccepted";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendEmergencyAccessConfirmedEmailAsync(string grantorName, string email)
{
var message = CreateDefaultMessage($"You Have Been Confirmed as Emergency Access Contact", email);
var model = new EmergencyAccessConfirmedViewModel
{
Name = CoreHelpers.SanitizeForEmail(grantorName),
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "EmergencyAccessConfirmed", model);
message.Category = "EmergencyAccessConfirmed";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendEmergencyAccessRecoveryInitiated(EmergencyAccess emergencyAccess, string initiatingName, string email)
{
var message = CreateDefaultMessage("Emergency Access Initiated", email);
var remainingTime = DateTime.UtcNow - emergencyAccess.RecoveryInitiatedDate.GetValueOrDefault();
var model = new EmergencyAccessRecoveryViewModel
{
Name = CoreHelpers.SanitizeForEmail(initiatingName),
Action = emergencyAccess.Type.ToString(),
DaysLeft = emergencyAccess.WaitTimeDays - Convert.ToInt32((remainingTime).TotalDays),
};
await AddMessageContentAsync(message, "EmergencyAccessRecovery", model);
message.Category = "EmergencyAccessRecovery";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendEmergencyAccessRecoveryApproved(EmergencyAccess emergencyAccess, string approvingName, string email)
{
var message = CreateDefaultMessage("Emergency Access Approved", email);
var model = new EmergencyAccessApprovedViewModel
{
Name = CoreHelpers.SanitizeForEmail(approvingName),
};
await AddMessageContentAsync(message, "EmergencyAccessApproved", model);
message.Category = "EmergencyAccessApproved";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendEmergencyAccessRecoveryRejected(EmergencyAccess emergencyAccess, string rejectingName, string email)
{
var message = CreateDefaultMessage("Emergency Access Rejected", email);
var model = new EmergencyAccessRejectedViewModel
{
Name = CoreHelpers.SanitizeForEmail(rejectingName),
};
await AddMessageContentAsync(message, "EmergencyAccessRejected", model);
message.Category = "EmergencyAccessRejected";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendEmergencyAccessRecoveryReminder(EmergencyAccess emergencyAccess, string initiatingName, string email)
{
var message = CreateDefaultMessage("Pending Emergency Access Request", email);
var remainingTime = DateTime.UtcNow - emergencyAccess.RecoveryInitiatedDate.GetValueOrDefault();
var model = new EmergencyAccessRecoveryViewModel
{
Name = CoreHelpers.SanitizeForEmail(initiatingName),
Action = emergencyAccess.Type.ToString(),
DaysLeft = emergencyAccess.WaitTimeDays - Convert.ToInt32((remainingTime).TotalDays),
};
await AddMessageContentAsync(message, "EmergencyAccessRecoveryReminder", model);
message.Category = "EmergencyAccessRecoveryReminder";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendEmergencyAccessRecoveryTimedOut(EmergencyAccess emergencyAccess, string initiatingName, string email)
{
var message = CreateDefaultMessage("Emergency Access Granted", email);
var model = new EmergencyAccessRecoveryTimedOutViewModel
{
Name = CoreHelpers.SanitizeForEmail(initiatingName),
Action = emergencyAccess.Type.ToString(),
};
await AddMessageContentAsync(message, "EmergencyAccessRecoveryTimedOut", model);
message.Category = "EmergencyAccessRecoveryTimedOut";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendProviderSetupInviteEmailAsync(Provider provider, string token, string email)
{
var message = CreateDefaultMessage($"Create a Provider", email);
var model = new ProviderSetupInviteViewModel
{
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
ProviderId = provider.Id.ToString(),
Email = WebUtility.UrlEncode(email),
Token = WebUtility.UrlEncode(token),
};
await AddMessageContentAsync(message, "Provider.ProviderSetupInvite", model);
message.Category = "ProviderSetupInvite";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendProviderInviteEmailAsync(string providerName, ProviderUser providerUser, string token, string email)
{
var message = CreateDefaultMessage($"Join {providerName}", email);
var model = new ProviderUserInvitedViewModel
{
ProviderName = CoreHelpers.SanitizeForEmail(providerName),
Email = WebUtility.UrlEncode(providerUser.Email),
ProviderId = providerUser.ProviderId.ToString(),
ProviderUserId = providerUser.Id.ToString(),
ProviderNameUrlEncoded = WebUtility.UrlEncode(providerName),
Token = WebUtility.UrlEncode(token),
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
};
await AddMessageContentAsync(message, "Provider.ProviderUserInvited", model);
message.Category = "ProviderSetupInvite";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendProviderConfirmedEmailAsync(string providerName, string email)
{
var message = CreateDefaultMessage($"You Have Been Confirmed To {providerName}", email);
var model = new ProviderUserConfirmedViewModel
{
ProviderName = CoreHelpers.SanitizeForEmail(providerName),
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "Provider.ProviderUserConfirmed", model);
message.Category = "ProviderUserConfirmed";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendProviderUserRemoved(string providerName, string email)
{
var message = CreateDefaultMessage($"You Have Been Removed from {providerName}", email);
var model = new ProviderUserRemovedViewModel
{
ProviderName = CoreHelpers.SanitizeForEmail(providerName),
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "Provider.ProviderUserRemoved", model);
message.Category = "ProviderUserRemoved";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendUpdatedTempPasswordEmailAsync(string email, string userName)
{
var message = CreateDefaultMessage("Master Password Has Been Changed", email);
var model = new UpdateTempPasswordViewModel()
{
UserName = CoreHelpers.SanitizeForEmail(userName)
};
await AddMessageContentAsync(message, "UpdatedTempPassword", model);
message.Category = "UpdatedTempPassword";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendFamiliesForEnterpriseOfferEmailAsync(string sponsorOrgName, string email, bool existingAccount, string token) =>
await BulkSendFamiliesForEnterpriseOfferEmailAsync(sponsorOrgName, new[] { (email, existingAccount, token) });
public async Task BulkSendFamiliesForEnterpriseOfferEmailAsync(string sponsorOrgName, IEnumerable<(string Email, bool ExistingAccount, string Token)> invites)
{
MailQueueMessage CreateMessage((string Email, bool ExistingAccount, string Token) invite)
{
var message = CreateDefaultMessage("Accept Your Free Families Subscription", invite.Email);
message.Category = "FamiliesForEnterpriseOffer";
var model = new FamiliesForEnterpriseOfferViewModel
{
SponsorOrgName = sponsorOrgName,
SponsoredEmail = WebUtility.UrlEncode(invite.Email),
ExistingAccount = invite.ExistingAccount,
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
SponsorshipToken = invite.Token,
};
var templateName = invite.ExistingAccount ?
"FamiliesForEnterprise.FamiliesForEnterpriseOfferExistingAccount" :
"FamiliesForEnterprise.FamiliesForEnterpriseOfferNewAccount";
return new MailQueueMessage(message, templateName, model);
}
var messageModels = invites.Select(invite => CreateMessage(invite));
await EnqueueMailAsync(messageModels);
}
public async Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail)
{
// Email family user
await SendFamiliesForEnterpriseInviteRedeemedToFamilyUserEmailAsync(familyUserEmail);
// Email enterprise org user
await SendFamiliesForEnterpriseInviteRedeemedToEnterpriseUserEmailAsync(sponsorEmail);
}
private async Task SendFamiliesForEnterpriseInviteRedeemedToFamilyUserEmailAsync(string email)
{
var message = CreateDefaultMessage("Success! Families Subscription Accepted", email);
await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseRedeemedToFamilyUser", new BaseMailModel());
message.Category = "FamilyForEnterpriseRedeemedToFamilyUser";
await _mailDeliveryService.SendEmailAsync(message);
}
private async Task SendFamiliesForEnterpriseInviteRedeemedToEnterpriseUserEmailAsync(string email)
{
var message = CreateDefaultMessage("Success! Families Subscription Accepted", email);
await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseRedeemedToEnterpriseUser", new BaseMailModel());
message.Category = "FamilyForEnterpriseRedeemedToEnterpriseUser";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate)
{
var message = CreateDefaultMessage("Your Families Sponsorship was Removed", email);
var model = new FamiliesForEnterpriseSponsorshipRevertingViewModel
{
ExpirationDate = expirationDate,
};
await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseSponsorshipReverting", model);
message.Category = "FamiliesForEnterpriseSponsorshipReverting";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendOTPEmailAsync(string email, string token)
{
var message = CreateDefaultMessage("Your Bitwarden Verification Code", email);
var model = new EmailTokenViewModel
{
Token = token,
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
};
await AddMessageContentAsync(message, "OTPEmail", model);
message.MetaData.Add("SendGridBypassListManagement", true);
message.Category = "OTP";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip)
{
var message = CreateDefaultMessage("Failed login attempts detected", email);
var model = new FailedAuthAttemptsModel()
{
TheDate = utcNow.ToLongDateString(),
TheTime = utcNow.ToShortTimeString(),
TimeZone = "UTC",
IpAddress = ip,
AffectedEmail = email
};
await AddMessageContentAsync(message, "FailedLoginAttempts", model);
message.Category = "FailedLoginAttempts";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip)
{
var message = CreateDefaultMessage("Failed login attempts detected", email);
var model = new FailedAuthAttemptsModel()
{
TheDate = utcNow.ToLongDateString(),
TheTime = utcNow.ToShortTimeString(),
TimeZone = "UTC",
IpAddress = ip,
AffectedEmail = email
};
await AddMessageContentAsync(message, "FailedTwoFactorAttempts", model);
message.Category = "FailedTwoFactorAttempts";
await _mailDeliveryService.SendEmailAsync(message);
}
}
}