diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteBaseView.cs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteBaseView.cs new file mode 100644 index 0000000000..0fd14233e1 --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteBaseView.cs @@ -0,0 +1,13 @@ +using Bit.Core.Platform.Mail.Mailer; + +namespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationInvite; + +public abstract class OrganizationInviteBaseView : BaseMailView +{ + public required string OrganizationName { get; set; } + public required string Email { get; set; } + public required string ExpirationDate { get; set; } + public required string Url { get; set; } + public required string ButtonText { get; set; } + public string? InviterEmail { get; set; } +} diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.cs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.cs new file mode 100644 index 0000000000..0459ab06c4 --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.cs @@ -0,0 +1,12 @@ +using Bit.Core.Platform.Mail.Mailer; + +namespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationInvite; + +public class OrganizationInviteEnterpriseTeamsExistingUserView : OrganizationInviteBaseView +{ +} + +public class OrganizationInviteEnterpriseTeamsExistingUser : BaseMail +{ + public override required string Subject { get; set; } +} diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.html.hbs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.html.hbs new file mode 100644 index 0000000000..72a0e667c0 --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.html.hbs @@ -0,0 +1,976 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +

+ {{OrganizationName}} invited you to join them on Bitwarden +

+ +
+ + + + + + + +
+ + {{ButtonText}} + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +
+ + +
+ +
+ + + + + +
+ + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
{{OrganizationName}} is rolling out Bitwarden to increase security and protect your sensitive data. Once you accept this invitation, you can:
+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Store Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Store logins securely so you never forget your passwords.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Autofill Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Sign in to accounts quickly by filling passwords with one click.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Share Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Share logins easily with your team.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
{{#if InviterEmail}} + This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}} + {{else}} + This invitation expires {{ExpirationDate}} + {{/if}}
+ +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+ +
Your existing account will be owned by {{OrganizationName}}
+ +
+ +
By accepting this invitation, your account ({{Email}}) will be owned by {{OrganizationName}} and will be subject to their security and management policies. Contact your administrator with any questions or concerns.
+ +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +

+ Learn more about Bitwarden +

+

+ Find user guides, product documentation, and videos on the + Bitwarden Help Center. +

+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + +
+ +

+ © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa + Barbara, CA, USA +

+

+ Always confirm you are on a trusted Bitwarden domain before logging + in:
+ bitwarden.com | + Learn why we include this +

+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + \ No newline at end of file diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.text.hbs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.text.hbs new file mode 100644 index 0000000000..18a7bfe759 --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.text.hbs @@ -0,0 +1,19 @@ +{{#>TitleContactUsTextLayout}} +{{OrganizationName}} is rolling out Bitwarden to increase security and protect your sensitive data. Once you accept this invitation, you can: + +- Store logins securely so you never forget your passwords. +- Sign in to accounts quickly by filling passwords with one click. +- Share logins easily with your team. + +{{#if InviterEmail}} +This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}. +{{else}} +This invitation expires {{ExpirationDate}}. +{{/if}} + +Your existing account will be owned by {{OrganizationName}} + +By accepting this invitation, your account ({{Email}}) will be owned by {{OrganizationName}} and will be subject to their security and management policies. Contact your administrator with any questions or concerns. + +Accept invitation: {{Url}} +{{/TitleContactUsTextLayout}} diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.cs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.cs new file mode 100644 index 0000000000..f43756ddc5 --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.cs @@ -0,0 +1,12 @@ +using Bit.Core.Platform.Mail.Mailer; + +namespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationInvite; + +public class OrganizationInviteEnterpriseTeamsNewUserView : OrganizationInviteBaseView +{ +} + +public class OrganizationInviteEnterpriseTeamsNewUser : BaseMail +{ + public override required string Subject { get; set; } +} diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.html.hbs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.html.hbs new file mode 100644 index 0000000000..59e12e899a --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.html.hbs @@ -0,0 +1,908 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +

+ {{OrganizationName}} set up a Bitwarden password manager account for you. +

+ +
+ + + + + + + +
+ + {{ButtonText}} + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +
+ + +
+ +
+ + + + + +
+ + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
{{OrganizationName}} is rolling out Bitwarden to increase security and protect your sensitive data. Once you finish account setup, you can:
+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Store Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Store logins securely so you never forget your passwords.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Autofill Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Sign in to accounts quickly by filling passwords with one click.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Share Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Share logins easily with your team.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
{{#if InviterEmail}} + This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}} + {{else}} + This invitation expires {{ExpirationDate}} + {{/if}}
+ +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +

+ Learn more about Bitwarden +

+

+ Find user guides, product documentation, and videos on the + Bitwarden Help Center. +

+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + +
+ +

+ © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa + Barbara, CA, USA +

+

+ Always confirm you are on a trusted Bitwarden domain before logging + in:
+ bitwarden.com | + Learn why we include this +

+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + \ No newline at end of file diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.text.hbs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.text.hbs new file mode 100644 index 0000000000..cc0ea9091f --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.text.hbs @@ -0,0 +1,15 @@ +{{#>TitleContactUsTextLayout}} +{{OrganizationName}} is rolling out Bitwarden to increase security and protect your sensitive data. Once you finish account setup, you can: + +- Store logins securely so you never forget your passwords. +- Sign in to accounts quickly by filling passwords with one click. +- Share logins easily with your team. + +{{#if InviterEmail}} +This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}. +{{else}} +This invitation expires {{ExpirationDate}}. +{{/if}} + +Finish account setup: {{Url}} +{{/TitleContactUsTextLayout}} diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.cs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.cs new file mode 100644 index 0000000000..1f955be88c --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.cs @@ -0,0 +1,12 @@ +using Bit.Core.Platform.Mail.Mailer; + +namespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationInvite; + +public class OrganizationInviteFamiliesExistingUserView : OrganizationInviteBaseView +{ +} + +public class OrganizationInviteFamiliesExistingUser : BaseMail +{ + public override required string Subject { get; set; } +} diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.html.hbs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.html.hbs new file mode 100644 index 0000000000..2e76a0fed0 --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.html.hbs @@ -0,0 +1,908 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +

+ {{OrganizationName}} invited you to join them on Bitwarden +

+ +
+ + + + + + + +
+ + {{ButtonText}} + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +
+ + +
+ +
+ + + + + +
+ + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
{{OrganizationName}} is using Bitwarden to simplify password sharing and protect your sensitive data. Once you accept this invitation, you can:
+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Store Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Store logins securely so you never forget your passwords.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Autofill Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Sign in to accounts quickly by filling passwords with one click.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Share Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Share logins easily with your friends, family, or coworkers.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
{{#if InviterEmail}} + This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}} + {{else}} + This invitation expires {{ExpirationDate}} + {{/if}}
+ +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +

+ Learn more about Bitwarden +

+

+ Find user guides, product documentation, and videos on the + Bitwarden Help Center. +

+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + +
+ +

+ © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa + Barbara, CA, USA +

+

+ Always confirm you are on a trusted Bitwarden domain before logging + in:
+ bitwarden.com | + Learn why we include this +

+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + \ No newline at end of file diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.text.hbs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.text.hbs new file mode 100644 index 0000000000..a2df4eb43a --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.text.hbs @@ -0,0 +1,15 @@ +{{#>TitleContactUsTextLayout}} +{{OrganizationName}} is using Bitwarden to simplify password sharing and protect your sensitive data. Once you accept this invitation, you can: + +- Store logins securely so you never forget your passwords. +- Sign in to accounts quickly by filling passwords with one click. +- Share logins easily with your friends, family, or coworkers. + +{{#if InviterEmail}} +This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}. +{{else}} +This invitation expires {{ExpirationDate}}. +{{/if}} + +Accept invitation: {{Url}} +{{/TitleContactUsTextLayout}} diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesNewUserView.cs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesNewUserView.cs new file mode 100644 index 0000000000..a9cc496855 --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesNewUserView.cs @@ -0,0 +1,12 @@ +using Bit.Core.Platform.Mail.Mailer; + +namespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationInvite; + +public class OrganizationInviteFamiliesNewUserView : OrganizationInviteBaseView +{ +} + +public class OrganizationInviteFamiliesNewUser : BaseMail +{ + public override required string Subject { get; set; } +} diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesNewUserView.html.hbs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesNewUserView.html.hbs new file mode 100644 index 0000000000..03d41f12e5 --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesNewUserView.html.hbs @@ -0,0 +1,908 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +

+ {{OrganizationName}} set up a Bitwarden password manager account for you. +

+ +
+ + + + + + + +
+ + {{ButtonText}} + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +
+ + +
+ +
+ + + + + +
+ + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
{{OrganizationName}} is using Bitwarden to simplify password sharing and protect your sensitive data. Once you finish account setup, you can:
+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Store Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Store logins securely so you never forget your passwords.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Autofill Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Sign in to accounts quickly by filling passwords with one click.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Share Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Share logins easily with your friends, family, or coworkers.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
{{#if InviterEmail}} + This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}} + {{else}} + This invitation expires {{ExpirationDate}} + {{/if}}
+ +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +

+ Learn more about Bitwarden +

+

+ Find user guides, product documentation, and videos on the + Bitwarden Help Center. +

+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + +
+ +

+ © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa + Barbara, CA, USA +

+

+ Always confirm you are on a trusted Bitwarden domain before logging + in:
+ bitwarden.com | + Learn why we include this +

+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + \ No newline at end of file diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesNewUserView.text.hbs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesNewUserView.text.hbs new file mode 100644 index 0000000000..071a056c40 --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesNewUserView.text.hbs @@ -0,0 +1,15 @@ +{{#>TitleContactUsTextLayout}} +{{OrganizationName}} is using Bitwarden to simplify password sharing and protect your sensitive data. Once you finish account setup, you can: + +- Store logins securely so you never forget your passwords. +- Sign in to accounts quickly by filling passwords with one click. +- Share logins easily with your friends, family, or coworkers. + +{{#if InviterEmail}} +This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}. +{{else}} +This invitation expires {{ExpirationDate}}. +{{/if}} + +Finish account setup: {{Url}} +{{/TitleContactUsTextLayout}} diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFreeView.cs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFreeView.cs new file mode 100644 index 0000000000..6347e85a6f --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFreeView.cs @@ -0,0 +1,12 @@ +using Bit.Core.Platform.Mail.Mailer; + +namespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationInvite; + +public class OrganizationInviteFreeView : OrganizationInviteBaseView +{ +} + +public class OrganizationInviteFree : BaseMail +{ + public override required string Subject { get; set; } +} diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFreeView.html.hbs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFreeView.html.hbs new file mode 100644 index 0000000000..d7c5acd126 --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFreeView.html.hbs @@ -0,0 +1,908 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +

+ You have been invited to Bitwarden Password Manager +

+ +
+ + + + + + + +
+ + {{ButtonText}} + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +
+ + +
+ +
+ + + + + +
+ + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
Bitwarden is a password manager used to simplify password sharing and protect your sensitive data. Once you accept this invitation, you can:
+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Store Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Securely store logins so you never forget your passwords.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Autofill Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Sign in to accounts quickly by filling passwords with one click.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Share Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ Share logins easily with your friends, family, or coworkers.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
{{#if InviterEmail}} + This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}} + {{else}} + This invitation expires {{ExpirationDate}} + {{/if}}
+ +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +

+ Learn more about Bitwarden +

+

+ Find user guides, product documentation, and videos on the + Bitwarden Help Center. +

+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + +
+ +

+ © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa + Barbara, CA, USA +

+

+ Always confirm you are on a trusted Bitwarden domain before logging + in:
+ bitwarden.com | + Learn why we include this +

+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + \ No newline at end of file diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFreeView.text.hbs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFreeView.text.hbs new file mode 100644 index 0000000000..fa4b7abaaf --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFreeView.text.hbs @@ -0,0 +1,15 @@ +{{#>TitleContactUsTextLayout}} +Bitwarden is a password manager used to simplify password sharing and protect your sensitive data. Once you accept this invitation, you can: + +- Securely store logins so you never forget your passwords. +- Sign in to accounts quickly by filling passwords with one click. +- Share logins easily with your friends, family, or coworkers. + +{{#if InviterEmail}} +This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}. +{{else}} +This invitation expires {{ExpirationDate}}. +{{/if}} + +Accept invitation: {{Url}} +{{/TitleContactUsTextLayout}} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs index 6899959b8d..abcc39aea0 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs @@ -150,7 +150,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService, await SendAdditionalEmailsAsync(validatedRequest, organization); - await SendInvitesAsync(organizationUserToInviteEntities, organization); + await SendInvitesAsync(organizationUserToInviteEntities, organization, request.PerformedBy); } catch (Exception ex) { @@ -212,11 +212,13 @@ public class InviteOrganizationUsersCommand(IEventService eventService, } } - private async Task SendInvitesAsync(IEnumerable users, Organization organization) => + private async Task SendInvitesAsync(IEnumerable users, Organization organization, Guid invitingUserId) => await sendOrganizationInvitesCommand.SendInvitesAsync( new SendInvitesRequest( users.Select(x => x.OrganizationUser), - organization)); + organization, + initOrganization: false, + invitingUserId: invitingUserId)); private async Task SendAdditionalEmailsAsync(Valid validatedResult, Organization organization) { diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/SendInvitesRequest.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/SendInvitesRequest.cs index 2be6430512..f92a6d6fe8 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/SendInvitesRequest.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/SendInvitesRequest.cs @@ -10,11 +10,8 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUse /// public class SendInvitesRequest { - public SendInvitesRequest(IEnumerable users, Organization organization) => - (Users, Organization) = (users.ToArray(), organization); - - public SendInvitesRequest(IEnumerable users, Organization organization, bool initOrganization) => - (Users, Organization, InitOrganization) = (users.ToArray(), organization, initOrganization); + public SendInvitesRequest(IEnumerable users, Organization organization, bool initOrganization = false, Guid? invitingUserId = null) => + (Users, Organization, InitOrganization, InvitingUserId) = (users.ToArray(), organization, initOrganization, invitingUserId); /// /// Organization Users to send emails to. @@ -30,4 +27,9 @@ public class SendInvitesRequest /// This is for when the organization is being created and this is the owners initial invite /// public bool InitOrganization { get; init; } + + /// + /// The user ID of the person sending the invitation (null for SCIM/automated invitations) + /// + public Guid? InvitingUserId { get; init; } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs index 61f428414f..4dc379d804 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs @@ -1,17 +1,23 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using System.Net; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationInvite; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.Auth.Models.Business; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Models.Mail; +using Bit.Core.Platform.Mail.Mailer; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Settings; using Bit.Core.Tokens; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; @@ -22,13 +28,39 @@ public class SendOrganizationInvitesCommand( IPolicyQuery policyQuery, IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory, IDataProtectorTokenFactory dataProtectorTokenFactory, - IMailService mailService) : ISendOrganizationInvitesCommand + IMailService mailService, + IMailer mailer, + IFeatureService featureService, + GlobalSettings globalSettings) : ISendOrganizationInvitesCommand { + // New user (no existing account) email constants + private const string _newUserSubject = "set up a Bitwarden account for you"; + private const string _newUserTitle = "set up a Bitwarden password manager account for you."; + private const string _newUserButton = "Finish account setup"; + + // Existing user email constants + private const string _existingUserSubject = "invited you to their Bitwarden organization"; + private const string _existingUserTitle = "invited you to join them on Bitwarden"; + private const string _existingUserButton = "Accept invitation"; + + // Free organization email constants + private const string _freeOrgNewUserSubject = "You have been invited to Bitwarden Password Manager"; + private const string _freeOrgExistingUserSubject = "You have been invited to a Bitwarden Organization"; + private const string _freeOrgTitle = "You have been invited to Bitwarden Password Manager"; + public async Task SendInvitesAsync(SendInvitesRequest request) { var orgInvitesInfo = await BuildOrganizationInvitesInfoAsync(request.Users, request.Organization, request.InitOrganization); - await mailService.SendOrganizationInviteEmailsAsync(orgInvitesInfo); + if (featureService.IsEnabled(FeatureFlagKeys.UpdateJoinOrganizationEmailTemplate)) + { + var inviterEmail = await GetInviterEmailAsync(request.InvitingUserId); + await SendNewInviteEmailsAsync(orgInvitesInfo, inviterEmail); + } + else + { + await mailService.SendOrganizationInviteEmailsAsync(orgInvitesInfo); + } } private async Task BuildOrganizationInvitesInfoAsync(IEnumerable orgUsers, @@ -80,4 +112,235 @@ public class SendOrganizationInvitesCommand( initOrganization ); } + + private async Task SendNewInviteEmailsAsync(OrganizationInvitesInfo orgInvitesInfo, string inviterEmail) + { + foreach (var (orgUser, token) in orgInvitesInfo.OrgUserTokenPairs) + { + var userHasExistingUser = orgInvitesInfo.OrgUserHasExistingUserDict[orgUser.Id]; + + await SendInviteEmailAsync( + userHasExistingUser, + orgInvitesInfo, + orgUser, + token, + inviterEmail + ); + } + } + + private async Task SendInviteEmailAsync( + bool userHasExistingUser, + OrganizationInvitesInfo orgInvitesInfo, + OrganizationUser orgUser, + ExpiringToken token, + string inviterEmail) + { + if (PlanConstants.EnterprisePlanTypes.Contains(orgInvitesInfo.PlanType) || + PlanConstants.TeamsPlanTypes.Contains(orgInvitesInfo.PlanType) || + orgInvitesInfo.PlanType == PlanType.TeamsStarter || + orgInvitesInfo.PlanType == PlanType.TeamsStarter2023 || + orgInvitesInfo.PlanType == PlanType.Custom) + { + if (userHasExistingUser) + { + await SendEnterpriseTeamsExistingUserInviteAsync(orgInvitesInfo, orgUser, token, inviterEmail); + } + else + { + await SendEnterpriseTeamsNewUserInviteAsync(orgInvitesInfo, orgUser, token, inviterEmail); + } + } + else if (PlanConstants.FamiliesPlanTypes.Contains(orgInvitesInfo.PlanType)) + { + if (userHasExistingUser) + { + await SendFamiliesExistingUserInviteAsync(orgInvitesInfo, orgUser, token, inviterEmail); + } + else + { + await SendFamiliesNewUserInviteAsync(orgInvitesInfo, orgUser, token, inviterEmail); + } + } + else + { + // Free plan (default) + await SendFreeOrganizationInviteAsync(orgInvitesInfo, orgUser, token, inviterEmail, userHasExistingUser); + } + } + + private async Task SendEnterpriseTeamsNewUserInviteAsync( + OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail) + { + var organizationName = WebUtility.HtmlDecode(orgInvitesInfo.OrganizationName); + var mail = new OrganizationInviteEnterpriseTeamsNewUser + { + ToEmails = [orgUser.Email], + Subject = $"{organizationName} {_newUserSubject}", + View = CreateEnterpriseTeamsNewUserView(orgInvitesInfo, orgUser, token, inviterEmail) + }; + await mailer.SendEmail(mail); + } + + private async Task SendEnterpriseTeamsExistingUserInviteAsync( + OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail) + { + var organizationName = WebUtility.HtmlDecode(orgInvitesInfo.OrganizationName); + var mail = new OrganizationInviteEnterpriseTeamsExistingUser + { + ToEmails = [orgUser.Email], + Subject = $"{organizationName} {_existingUserSubject}", + View = CreateEnterpriseTeamsExistingUserView(orgInvitesInfo, orgUser, token, inviterEmail) + }; + await mailer.SendEmail(mail); + } + + private async Task SendFamiliesNewUserInviteAsync( + OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail) + { + var organizationName = WebUtility.HtmlDecode(orgInvitesInfo.OrganizationName); + var mail = new OrganizationInviteFamiliesNewUser + { + ToEmails = [orgUser.Email], + Subject = $"{organizationName} {_newUserSubject}", + View = CreateFamiliesNewUserView(orgInvitesInfo, orgUser, token, inviterEmail) + }; + await mailer.SendEmail(mail); + } + + private async Task SendFamiliesExistingUserInviteAsync( + OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail) + { + var organizationName = WebUtility.HtmlDecode(orgInvitesInfo.OrganizationName); + var mail = new OrganizationInviteFamiliesExistingUser + { + ToEmails = [orgUser.Email], + Subject = $"{organizationName} {_existingUserSubject}", + View = CreateFamiliesExistingUserView(orgInvitesInfo, orgUser, token, inviterEmail) + }; + await mailer.SendEmail(mail); + } + + private async Task SendFreeOrganizationInviteAsync( + OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail, bool userHasExistingUser) + { + var mail = new OrganizationInviteFree + { + ToEmails = [orgUser.Email], + Subject = userHasExistingUser ? _freeOrgExistingUserSubject : _freeOrgNewUserSubject, + View = CreateFreeView(orgInvitesInfo, orgUser, token, inviterEmail) + }; + await mailer.SendEmail(mail); + } + + private OrganizationInviteEnterpriseTeamsNewUserView CreateEnterpriseTeamsNewUserView( + OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail) + { + var organizationName = WebUtility.HtmlDecode(orgInvitesInfo.OrganizationName); + return new OrganizationInviteEnterpriseTeamsNewUserView + { + OrganizationName = organizationName, + Email = orgUser.Email, + ExpirationDate = FormatExpirationDate(token.ExpirationDate), + Url = BuildInvitationUrl(orgInvitesInfo, orgUser, token), + ButtonText = _newUserButton, + InviterEmail = inviterEmail + }; + } + + private OrganizationInviteEnterpriseTeamsExistingUserView CreateEnterpriseTeamsExistingUserView( + OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail) + { + var organizationName = WebUtility.HtmlDecode(orgInvitesInfo.OrganizationName); + return new OrganizationInviteEnterpriseTeamsExistingUserView + { + OrganizationName = organizationName, + Email = orgUser.Email, + ExpirationDate = FormatExpirationDate(token.ExpirationDate), + Url = BuildInvitationUrl(orgInvitesInfo, orgUser, token), + ButtonText = _existingUserButton, + InviterEmail = inviterEmail + }; + } + + private OrganizationInviteFamiliesNewUserView CreateFamiliesNewUserView( + OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail) + { + var organizationName = WebUtility.HtmlDecode(orgInvitesInfo.OrganizationName); + return new OrganizationInviteFamiliesNewUserView + { + OrganizationName = organizationName, + Email = orgUser.Email, + ExpirationDate = FormatExpirationDate(token.ExpirationDate), + Url = BuildInvitationUrl(orgInvitesInfo, orgUser, token), + ButtonText = _newUserButton, + InviterEmail = inviterEmail + }; + } + + private OrganizationInviteFamiliesExistingUserView CreateFamiliesExistingUserView( + OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail) + { + var organizationName = WebUtility.HtmlDecode(orgInvitesInfo.OrganizationName); + return new OrganizationInviteFamiliesExistingUserView + { + OrganizationName = organizationName, + Email = orgUser.Email, + ExpirationDate = FormatExpirationDate(token.ExpirationDate), + Url = BuildInvitationUrl(orgInvitesInfo, orgUser, token), + ButtonText = _existingUserButton, + InviterEmail = inviterEmail + }; + } + + private OrganizationInviteFreeView CreateFreeView( + OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token, string inviterEmail) + { + var organizationName = WebUtility.HtmlDecode(orgInvitesInfo.OrganizationName); + return new OrganizationInviteFreeView + { + OrganizationName = organizationName, + Email = orgUser.Email, + ExpirationDate = FormatExpirationDate(token.ExpirationDate), + Url = BuildInvitationUrl(orgInvitesInfo, orgUser, token), + ButtonText = _existingUserButton, + InviterEmail = inviterEmail + }; + } + + private string BuildInvitationUrl(OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token) + { + var baseUrl = $"{globalSettings.BaseServiceUri.VaultWithHash}/accept-organization"; + var queryParams = new List + { + $"organizationId={orgUser.OrganizationId}", + $"organizationUserId={orgUser.Id}", + $"email={WebUtility.UrlEncode(orgUser.Email)}", + $"organizationName={WebUtility.UrlEncode(orgInvitesInfo.OrganizationName)}", + $"token={WebUtility.UrlEncode(token.Token)}", + $"initOrganization={orgInvitesInfo.InitOrganization}", + $"orgUserHasExistingUser={orgInvitesInfo.OrgUserHasExistingUserDict[orgUser.Id]}" + }; + + if (orgInvitesInfo.OrgSsoEnabled && orgInvitesInfo.OrgSsoLoginRequiredPolicyEnabled) + { + queryParams.Add($"orgSsoIdentifier={orgInvitesInfo.OrgSsoIdentifier}"); + } + + return $"{baseUrl}?{string.Join("&", queryParams)}"; + } + + private async Task GetInviterEmailAsync(Guid? invitingUserId) + { + if (!invitingUserId.HasValue) + { + return null; + } + + var invitingUser = await userRepository.GetByIdAsync(invitingUserId.Value); + return invitingUser?.Email; + } + + private static string FormatExpirationDate(DateTime expirationDate) => + $"{expirationDate.ToLongDateString()} {expirationDate.ToShortTimeString()} UTC"; } diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index d87bc65042..109128b114 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -506,7 +506,7 @@ public class OrganizationService : IOrganizationService } } - var (organizationUsers, events) = await SaveUsersSendInvitesAsync(organizationId, invites); + var (organizationUsers, events) = await SaveUsersSendInvitesAsync(organizationId, invites, invitingUserId); if (systemUser.HasValue) { @@ -526,7 +526,8 @@ public class OrganizationService : IOrganizationService private async Task<(List organizationUsers, List<(OrganizationUser, EventType, DateTime?)> events)> SaveUsersSendInvitesAsync(Guid organizationId, - IEnumerable<(OrganizationUserInvite invite, string externalId)> invites) + IEnumerable<(OrganizationUserInvite invite, string externalId)> invites, + Guid? invitingUserId) { var organization = await GetOrgById(organizationId); var initialSeatCount = organization.Seats; @@ -678,7 +679,7 @@ public class OrganizationService : IOrganizationService await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(smSubscriptionUpdate); } - await SendInvitesAsync(allOrgUsers, organization); + await SendInvitesAsync(allOrgUsers, organization, invitingUserId); } catch (Exception e) { @@ -717,14 +718,15 @@ public class OrganizationService : IOrganizationService return (allOrgUsers, events); } - private async Task SendInvitesAsync(IEnumerable orgUsers, Organization organization) => - await _sendOrganizationInvitesCommand.SendInvitesAsync(new SendInvitesRequest(orgUsers, organization)); + private async Task SendInvitesAsync(IEnumerable orgUsers, Organization organization, Guid? invitingUserId = null) => + await _sendOrganizationInvitesCommand.SendInvitesAsync(new SendInvitesRequest(orgUsers, organization, initOrganization: false, invitingUserId: invitingUserId)); - private async Task SendInviteAsync(OrganizationUser orgUser, Organization organization, bool initOrganization) => + private async Task SendInviteAsync(OrganizationUser orgUser, Organization organization, bool initOrganization, Guid? invitingUserId = null) => await _sendOrganizationInvitesCommand.SendInvitesAsync(new SendInvitesRequest( users: [orgUser], organization: organization, - initOrganization: initOrganization)); + initOrganization: initOrganization, + invitingUserId: invitingUserId)); public async Task<(bool canScale, string failureReason)> CanScaleAsync( Organization organization, diff --git a/src/Core/Billing/Constants/PlanConstants.cs b/src/Core/Billing/Constants/PlanConstants.cs index 1ac5b8e750..dfe6da7ef5 100644 --- a/src/Core/Billing/Constants/PlanConstants.cs +++ b/src/Core/Billing/Constants/PlanConstants.cs @@ -27,4 +27,11 @@ public static class PlanConstants PlanType.TeamsMonthly2023, PlanType.TeamsMonthly ]; + + public static List FamiliesPlanTypes => + [ + PlanType.FamiliesAnnually2019, + PlanType.FamiliesAnnually2025, + PlanType.FamiliesAnnually + ]; } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index ffa53b21df..fd571697e3 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -143,6 +143,7 @@ public static class FeatureFlagKeys public const string DefaultUserCollectionRestore = "pm-30883-my-items-restored-users"; public const string PremiumAccessQuery = "pm-29495-refactor-premium-interface"; public const string BulkReinviteUI = "pm-28416-bulk-reinvite-ux-improvements"; + public const string UpdateJoinOrganizationEmailTemplate = "pm-28396-update-join-organization-email-template"; /* Architecture */ public const string DesktopMigrationMilestone1 = "desktop-ui-migration-milestone-1"; diff --git a/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-enterprise-teams-existing-user.mjml b/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-enterprise-teams-existing-user.mjml new file mode 100644 index 0000000000..26dd97f693 --- /dev/null +++ b/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-enterprise-teams-existing-user.mjml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + {{OrganizationName}} is rolling out Bitwarden to increase security and protect your sensitive data. Once you accept this invitation, you can: + + + + + + + + + + {{#if InviterEmail}} + This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}} + {{else}} + This invitation expires {{ExpirationDate}} + {{/if}} + + + + + + + + + + + Your existing account will be owned by {{OrganizationName}} + + + By accepting this invitation, your account ({{Email}}) will be owned by {{OrganizationName}} and will be subject to their security and management policies. Contact your administrator with any questions or concerns. + + + + + + + + + + + + + + diff --git a/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-enterprise-teams-new-user.mjml b/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-enterprise-teams-new-user.mjml new file mode 100644 index 0000000000..cc9719d992 --- /dev/null +++ b/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-enterprise-teams-new-user.mjml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + {{OrganizationName}} is rolling out Bitwarden to increase security and protect your sensitive data. Once you finish account setup, you can: + + + + + + + + + + {{#if InviterEmail}} + This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}} + {{else}} + This invitation expires {{ExpirationDate}} + {{/if}} + + + + + + + + + + + + + + diff --git a/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-families-existing-user.mjml b/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-families-existing-user.mjml new file mode 100644 index 0000000000..581fc62e79 --- /dev/null +++ b/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-families-existing-user.mjml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + {{OrganizationName}} is using Bitwarden to simplify password sharing and protect your sensitive data. Once you accept this invitation, you can: + + + + + + + + + + {{#if InviterEmail}} + This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}} + {{else}} + This invitation expires {{ExpirationDate}} + {{/if}} + + + + + + + + + + + + + + diff --git a/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-families-new-user.mjml b/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-families-new-user.mjml new file mode 100644 index 0000000000..80110e1d0e --- /dev/null +++ b/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-families-new-user.mjml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + {{OrganizationName}} is using Bitwarden to simplify password sharing and protect your sensitive data. Once you finish account setup, you can: + + + + + + + + + + {{#if InviterEmail}} + This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}} + {{else}} + This invitation expires {{ExpirationDate}} + {{/if}} + + + + + + + + + + + + + + diff --git a/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-free.mjml b/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-free.mjml new file mode 100644 index 0000000000..35ac5f17c0 --- /dev/null +++ b/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-free.mjml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + Bitwarden is a password manager used to simplify password sharing and protect your sensitive data. Once you accept this invitation, you can: + + + + + + + + + + {{#if InviterEmail}} + This invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}} + {{else}} + This invitation expires {{ExpirationDate}} + {{/if}} + + + + + + + + + + + + + + diff --git a/src/Core/MailTemplates/Mjml/emails/AdminConsole/components/mj-bw-ac-icon-row.js b/src/Core/MailTemplates/Mjml/emails/AdminConsole/components/mj-bw-ac-icon-row.js index 8d3ac80a2a..0ed62916c4 100644 --- a/src/Core/MailTemplates/Mjml/emails/AdminConsole/components/mj-bw-ac-icon-row.js +++ b/src/Core/MailTemplates/Mjml/emails/AdminConsole/components/mj-bw-ac-icon-row.js @@ -42,6 +42,12 @@ class MjBwAcIconRow extends BodyComponent { .mj-bw-ac-icon-row-text-column { width: 100% !important; } + .mj-bw-ac-icon-row-bullet { + display: block !important; + } + .mj-bw-ac-icon-row-text-inline { + display: none !important; + } } `; }; @@ -89,7 +95,8 @@ class MjBwAcIconRow extends BodyComponent { ${headAnchorElement} - ${this.getAttribute("text")} + + ${this.getAttribute("text")} ${footAnchorElement} diff --git a/src/Core/Models/Mail/OrganizationInvitesInfo.cs b/src/Core/Models/Mail/OrganizationInvitesInfo.cs index af53f23a8a..9ba80d8299 100644 --- a/src/Core/Models/Mail/OrganizationInvitesInfo.cs +++ b/src/Core/Models/Mail/OrganizationInvitesInfo.cs @@ -20,6 +20,7 @@ public class OrganizationInvitesInfo { OrganizationName = org.DisplayName(); OrgSsoIdentifier = org.Identifier; + PlanType = org.PlanType; IsFreeOrg = org.PlanType == PlanType.Free; InitOrganization = initOrganization; @@ -32,6 +33,7 @@ public class OrganizationInvitesInfo } public string OrganizationName { get; } + public PlanType PlanType { get; } public bool IsFreeOrg { get; } public bool InitOrganization { get; } = false; public bool OrgSsoEnabled { get; } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommandTests.cs index ddede2d191..a4a6858477 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommandTests.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationInvite; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; @@ -10,6 +11,8 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Models.Mail; +using Bit.Core.Platform.Mail.Mailer; +using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Test.AdminConsole.AutoFixture; using Bit.Core.Test.AutoFixture.OrganizationFixtures; @@ -20,6 +23,7 @@ using Bit.Test.Common.Fakes; using NSubstitute; using NSubstitute.ReturnsExtensions; using Xunit; +using CoreGlobalSettings = Bit.Core.Settings.GlobalSettings; namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; @@ -111,4 +115,391 @@ public class SendOrganizationInvitesCommandTests info.IsFreeOrg == (organization.PlanType == PlanType.Free) && info.OrganizationName == organization.Name)); } + + [Theory] + [BitAutoData(PlanType.EnterpriseAnnually, false)] + [BitAutoData(PlanType.EnterpriseMonthly2023, false)] + [BitAutoData(PlanType.TeamsAnnually, false)] + [BitAutoData(PlanType.TeamsStarter, false)] + [BitAutoData(PlanType.EnterpriseAnnually, true)] + [BitAutoData(PlanType.TeamsAnnually, true)] + public async Task SendInvitesAsync_WithFeatureFlag_EnterpriseAndTeamsPlans_SendsEnterpriseTemplate( + PlanType planType, + bool userExists, + Organization organization, + OrganizationUser invite, + User invitingUser, + SutProvider sutProvider) + { + SetupSutProvider(sutProvider); + + // Arrange + organization.PlanType = planType; + invite.OrganizationId = organization.Id; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.UpdateJoinOrganizationEmailTemplate) + .Returns(true); + + sutProvider.GetDependency() + .GetManyByEmailsAsync(Arg.Any>()) + .Returns(userExists ? [new User { Email = invite.Email }] : []); + + sutProvider.GetDependency() + .GetByIdAsync(invitingUser.Id) + .Returns(invitingUser); + + sutProvider.GetDependency() + .CreateToken(Arg.Any()) + .Returns(info => new OrgUserInviteTokenable(info.Arg()) + { + ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) + }); + + // Act + await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, invitingUser.Id)); + + // Assert + if (userExists) + { + await sutProvider.GetDependency().Received(1) + .SendEmail(Arg.Any()); + } + else + { + await sutProvider.GetDependency().Received(1) + .SendEmail(Arg.Any()); + } + } + + [Theory] + [BitAutoData(PlanType.FamiliesAnnually, false)] + [BitAutoData(PlanType.FamiliesAnnually2025, false)] + [BitAutoData(PlanType.FamiliesAnnually, true)] + public async Task SendInvitesAsync_WithFeatureFlag_FamiliesPlans_SendsFamiliesTemplate( + PlanType planType, + bool userExists, + Organization organization, + OrganizationUser invite, + User invitingUser, + SutProvider sutProvider) + { + SetupSutProvider(sutProvider); + + // Arrange + organization.PlanType = planType; + invite.OrganizationId = organization.Id; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.UpdateJoinOrganizationEmailTemplate) + .Returns(true); + + sutProvider.GetDependency() + .GetManyByEmailsAsync(Arg.Any>()) + .Returns(userExists ? [new User { Email = invite.Email }] : []); + + sutProvider.GetDependency() + .GetByIdAsync(invitingUser.Id) + .Returns(invitingUser); + + sutProvider.GetDependency() + .CreateToken(Arg.Any()) + .Returns(info => new OrgUserInviteTokenable(info.Arg()) + { + ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) + }); + + // Act + await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, invitingUser.Id)); + + // Assert + if (userExists) + { + await sutProvider.GetDependency().Received(1) + .SendEmail(Arg.Any()); + } + else + { + await sutProvider.GetDependency().Received(1) + .SendEmail(Arg.Any()); + } + } + + [Theory, BitAutoData] + public async Task SendInvitesAsync_WithFeatureFlag_FreePlan_SendsFreeTemplate( + Organization organization, + OrganizationUser invite, + User invitingUser, + SutProvider sutProvider) + { + SetupSutProvider(sutProvider); + + // Arrange + organization.PlanType = PlanType.Free; + invite.OrganizationId = organization.Id; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.UpdateJoinOrganizationEmailTemplate) + .Returns(true); + + sutProvider.GetDependency() + .GetManyByEmailsAsync(Arg.Any>()) + .Returns([]); + + sutProvider.GetDependency() + .GetByIdAsync(invitingUser.Id) + .Returns(invitingUser); + + sutProvider.GetDependency() + .CreateToken(Arg.Any()) + .Returns(info => new OrgUserInviteTokenable(info.Arg()) + { + ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) + }); + + // Act + await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, invitingUser.Id)); + + // Assert + await sutProvider.GetDependency().Received(1) + .SendEmail(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task SendInvitesAsync_WithFeatureFlag_CustomPlan_SendsEnterpriseTemplate( + Organization organization, + OrganizationUser invite, + User invitingUser, + SutProvider sutProvider) + { + SetupSutProvider(sutProvider); + + // Arrange + organization.PlanType = PlanType.Custom; + invite.OrganizationId = organization.Id; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.UpdateJoinOrganizationEmailTemplate) + .Returns(true); + + sutProvider.GetDependency() + .GetManyByEmailsAsync(Arg.Any>()) + .Returns([]); + + sutProvider.GetDependency() + .GetByIdAsync(invitingUser.Id) + .Returns(invitingUser); + + sutProvider.GetDependency() + .CreateToken(Arg.Any()) + .Returns(info => new OrgUserInviteTokenable(info.Arg()) + { + ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) + }); + + // Act + await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, invitingUser.Id)); + + // Assert + await sutProvider.GetDependency().Received(1) + .SendEmail(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task SendInvitesAsync_WithFeatureFlagEnabled_UsesNewMailer( + Organization organization, + OrganizationUser invite, + User invitingUser, + SutProvider sutProvider) + { + SetupSutProvider(sutProvider); + + // Arrange + organization.PlanType = PlanType.EnterpriseAnnually; + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.UpdateJoinOrganizationEmailTemplate) + .Returns(true); + + sutProvider.GetDependency() + .GetManyByEmailsAsync(Arg.Any>()) + .Returns([new User { Email = invite.Email }]); + + sutProvider.GetDependency() + .GetByIdAsync(invitingUser.Id) + .Returns(invitingUser); + + sutProvider.GetDependency() + .CreateToken(Arg.Any()) + .Returns(info => new OrgUserInviteTokenable(info.Arg()) + { + ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) + }); + + // Act + await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, invitingUser.Id)); + + // Assert - verify new mailer is called, not legacy mail service + await sutProvider.GetDependency() + .Received(1) + .SendEmail(Arg.Any()); + + await sutProvider.GetDependency() + .DidNotReceive() + .SendOrganizationInviteEmailsAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task SendInvitesAsync_WithFeatureFlagDisabled_UsesLegacyMailService( + Organization organization, + OrganizationUser invite, + SutProvider sutProvider) + { + // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks + sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); + sutProvider.Create(); + + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.UpdateJoinOrganizationEmailTemplate) + .Returns(false); + + sutProvider.GetDependency() + .CreateToken(Arg.Any()) + .Returns(info => new OrgUserInviteTokenable(info.Arg()) + { + ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) + }); + + // Act + await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization)); + + // Assert - verify legacy mail service is called, not new mailer + await sutProvider.GetDependency() + .Received(1) + .SendOrganizationInviteEmailsAsync(Arg.Any()); + + await sutProvider.GetDependency() + .DidNotReceive() + .SendEmail(Arg.Any>()); + } + + [Theory, BitAutoData] + public async Task SendInvitesAsync_WithInvitingUserId_PopulatesInviterEmail( + Organization organization, + OrganizationUser invite, + User invitingUser, + SutProvider sutProvider) + { + SetupSutProvider(sutProvider); + + // Arrange + organization.PlanType = PlanType.EnterpriseAnnually; + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.UpdateJoinOrganizationEmailTemplate) + .Returns(true); + + sutProvider.GetDependency() + .GetManyByEmailsAsync(Arg.Any>()) + .Returns([]); + + sutProvider.GetDependency() + .GetByIdAsync(invitingUser.Id) + .Returns(invitingUser); + + sutProvider.GetDependency() + .CreateToken(Arg.Any()) + .Returns(info => new OrgUserInviteTokenable(info.Arg()) + { + ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) + }); + + // Act + await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, invitingUser.Id)); + + // Assert + await sutProvider.GetDependency().Received(1) + .SendEmail(Arg.Is(mail => + mail.View.InviterEmail == invitingUser.Email)); + } + + [Theory, BitAutoData] + public async Task SendInvitesAsync_WithNullInvitingUserId_SendsEmailWithoutInviter( + Organization organization, + OrganizationUser invite, + SutProvider sutProvider) + { + SetupSutProvider(sutProvider); + + // Arrange + organization.PlanType = PlanType.EnterpriseAnnually; + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.UpdateJoinOrganizationEmailTemplate) + .Returns(true); + + sutProvider.GetDependency() + .GetManyByEmailsAsync(Arg.Any>()) + .Returns([]); + + sutProvider.GetDependency() + .CreateToken(Arg.Any()) + .Returns(info => new OrgUserInviteTokenable(info.Arg()) + { + ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) + }); + + // Act - pass null for InvitingUserId + await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, null)); + + // Assert + await sutProvider.GetDependency().Received(1) + .SendEmail(Arg.Is(mail => + mail.View.InviterEmail == null)); + } + + [Theory, BitAutoData] + public async Task SendInvitesAsync_WithNonExistentInvitingUserId_SendsEmailWithoutInviter( + Organization organization, + OrganizationUser invite, + Guid nonExistentUserId, + SutProvider sutProvider) + { + SetupSutProvider(sutProvider); + + // Arrange + organization.PlanType = PlanType.EnterpriseAnnually; + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.UpdateJoinOrganizationEmailTemplate) + .Returns(true); + + sutProvider.GetDependency() + .GetManyByEmailsAsync(Arg.Any>()) + .Returns([]); + + // Mock GetByIdAsync to return null for non-existent user + sutProvider.GetDependency() + .GetByIdAsync(nonExistentUserId) + .ReturnsNull(); + + sutProvider.GetDependency() + .CreateToken(Arg.Any()) + .Returns(info => new OrgUserInviteTokenable(info.Arg()) + { + ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) + }); + + // Act + await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, nonExistentUserId)); + + // Assert + await sutProvider.GetDependency().Received(1) + .SendEmail(Arg.Is(mail => + mail.View.InviterEmail == null)); + } + + private void SetupSutProvider(SutProvider sutProvider) + { + sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); + sutProvider.SetDependency(new CoreGlobalSettings { BaseServiceUri = new CoreGlobalSettings.BaseServiceUriSettings(new CoreGlobalSettings()) }, "globalSettings"); + sutProvider.Create(); + } }