diff --git a/Streetwriters.Common/Helpers/HtmlHelper.cs b/Streetwriters.Common/Helpers/HtmlHelper.cs
new file mode 100644
index 0000000..78e85c0
--- /dev/null
+++ b/Streetwriters.Common/Helpers/HtmlHelper.cs
@@ -0,0 +1,23 @@
+using System.IO;
+using WebMarkupMin.Core;
+using WebMarkupMin.Core.Loggers;
+
+namespace Streetwriters.Common.Helpers
+{
+ public static class HtmlHelper
+ {
+ public static string ReadMinifiedHtmlFile(string path)
+ {
+ var settings = new HtmlMinificationSettings()
+ {
+ WhitespaceMinificationMode = WhitespaceMinificationMode.Medium,
+ };
+ var cssMinifier = new KristensenCssMinifier();
+ var jsMinifier = new CrockfordJsMinifier();
+
+ var minifier = new HtmlMinifier(settings, cssMinifier, jsMinifier, new NullLogger());
+
+ return minifier.Minify(File.ReadAllText(path), false).MinifiedContent;
+ }
+ }
+}
diff --git a/Streetwriters.Common/Interfaces/IEmailSender.cs b/Streetwriters.Common/Interfaces/IEmailSender.cs
new file mode 100644
index 0000000..ab6cbcd
--- /dev/null
+++ b/Streetwriters.Common/Interfaces/IEmailSender.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using MimeKit;
+using MimeKit.Cryptography;
+using Streetwriters.Common.Models;
+
+namespace Streetwriters.Common.Interfaces
+{
+ public interface IEmailSender
+ {
+ Task SendEmailAsync(
+ string email,
+ EmailTemplate template,
+ IClient client,
+ GnuPGContext gpgContext = null,
+ Dictionary attachments = null
+ );
+ }
+}
diff --git a/Streetwriters.Common/Models/EmailTemplate.cs b/Streetwriters.Common/Models/EmailTemplate.cs
new file mode 100644
index 0000000..76bd162
--- /dev/null
+++ b/Streetwriters.Common/Models/EmailTemplate.cs
@@ -0,0 +1,11 @@
+namespace Streetwriters.Common.Models
+{
+ public class EmailTemplate
+ {
+ public int? Id { get; set; }
+ public object Data { get; set; }
+ public string Subject { get; set; }
+ public string Html { get; set; }
+ public string Text { get; set; }
+ }
+}
diff --git a/Streetwriters.Common/Services/EmailSender.cs b/Streetwriters.Common/Services/EmailSender.cs
new file mode 100644
index 0000000..61d1393
--- /dev/null
+++ b/Streetwriters.Common/Services/EmailSender.cs
@@ -0,0 +1,134 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using MailKit.Net.Smtp;
+using MimeKit;
+using MimeKit.Cryptography;
+using Org.BouncyCastle.Bcpg;
+using Scriban;
+using Streetwriters.Common.Interfaces;
+using Streetwriters.Common.Models;
+
+namespace Streetwriters.Common.Services
+{
+ public class EmailSender : IEmailSender, IAsyncDisposable
+ {
+ private readonly SmtpClient mailClient = new();
+
+ public async Task SendEmailAsync(
+ string email,
+ EmailTemplate template,
+ IClient client,
+ GnuPGContext gpgContext = null,
+ Dictionary attachments = null
+ )
+ {
+ if (!mailClient.IsConnected)
+ {
+ if (int.TryParse(Common.Constants.SMTP_PORT, out int port))
+ {
+ await mailClient.ConnectAsync(
+ Common.Constants.SMTP_HOST,
+ port,
+ MailKit.Security.SecureSocketOptions.Auto
+ );
+ }
+ else
+ {
+ throw new InvalidDataException("SMTP_PORT is not a valid integer value.");
+ }
+ }
+
+ if (!mailClient.IsAuthenticated)
+ await mailClient.AuthenticateAsync(
+ Common.Constants.SMTP_USERNAME,
+ Common.Constants.SMTP_PASSWORD
+ );
+
+ var message = new MimeMessage();
+ var sender = new MailboxAddress(client.SenderName, client.SenderEmail);
+ message.From.Add(sender);
+ message.To.Add(new MailboxAddress("", email));
+ message.Subject = await Template.Parse(template.Subject).RenderAsync(template.Data);
+
+ if (!string.IsNullOrEmpty(Common.Constants.SMTP_REPLYTO_EMAIL))
+ message.ReplyTo.Add(MailboxAddress.Parse(Common.Constants.SMTP_REPLYTO_EMAIL));
+
+ message.Body = await GetEmailBodyAsync(
+ template,
+ client,
+ sender,
+ gpgContext,
+ attachments
+ );
+
+ await mailClient.SendAsync(message);
+ }
+
+ private static async Task GetEmailBodyAsync(
+ EmailTemplate template,
+ IClient client,
+ MailboxAddress sender,
+ GnuPGContext gpgContext = null,
+ Dictionary attachments = null
+ )
+ {
+ var builder = new BodyBuilder();
+ try
+ {
+ builder.TextBody = await Template.Parse(template.Text).RenderAsync(template.Data);
+ builder.HtmlBody = await Template.Parse(template.Html).RenderAsync(template.Data);
+
+ if (attachments != null)
+ {
+ foreach (var attachment in attachments)
+ {
+ builder.Attachments.Add(attachment.Key, attachment.Value);
+ }
+ }
+
+ var key = gpgContext?.GetSigningKey(sender);
+ if (key != null)
+ {
+ using (MemoryStream outputStream = new())
+ {
+ using (Stream armoredStream = new ArmoredOutputStream(outputStream))
+ {
+ key.PublicKey.Encode(armoredStream);
+ }
+ outputStream.Seek(0, SeekOrigin.Begin);
+ builder.Attachments.Add(
+ $"{client.Id}_pub.asc",
+ Encoding.ASCII.GetBytes(
+ Encoding.ASCII.GetString(outputStream.ToArray())
+ )
+ );
+ }
+ return await MultipartSigned.CreateAsync(
+ gpgContext,
+ sender,
+ DigestAlgorithm.Sha256,
+ builder.ToMessageBody()
+ );
+ }
+ else
+ {
+ return builder.ToMessageBody();
+ }
+ }
+ catch (Exception ex)
+ {
+ await Slogger.Error("GetEmailBodyAsync", ex.ToString());
+ return builder.ToMessageBody();
+ }
+ }
+
+ async ValueTask IAsyncDisposable.DisposeAsync()
+ {
+ await mailClient.DisconnectAsync(true);
+ mailClient.Dispose();
+ }
+ }
+}
diff --git a/Streetwriters.Common/Streetwriters.Common.csproj b/Streetwriters.Common/Streetwriters.Common.csproj
index 64fb436..0399971 100644
--- a/Streetwriters.Common/Streetwriters.Common.csproj
+++ b/Streetwriters.Common/Streetwriters.Common.csproj
@@ -6,13 +6,16 @@
+
+
+
diff --git a/Streetwriters.Identity/Controllers/AccountController.cs b/Streetwriters.Identity/Controllers/AccountController.cs
index 3fdf004..a9b2911 100644
--- a/Streetwriters.Identity/Controllers/AccountController.cs
+++ b/Streetwriters.Identity/Controllers/AccountController.cs
@@ -52,10 +52,8 @@ namespace Streetwriters.Identity.Controllers
{
private IPersistedGrantStore PersistedGrantStore { get; set; }
private ITokenGenerationService TokenGenerationService { get; set; }
- private IUserClaimsPrincipalFactory PrincipalFactory { get; set; }
- private IdentityServerOptions ISOptions { get; set; }
private IUserAccountService UserAccountService { get; set; }
- public AccountController(UserManager _userManager, IEmailSender _emailSender,
+ public AccountController(UserManager _userManager, ITemplatedEmailSender _emailSender,
SignInManager _signInManager, RoleManager _roleManager, IPersistedGrantStore store,
ITokenGenerationService tokenGenerationService, IMFAService _mfaService, IUserAccountService userAccountService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService)
{
@@ -341,4 +339,4 @@ namespace Streetwriters.Identity.Controllers
});
}
}
-}
\ No newline at end of file
+}
diff --git a/Streetwriters.Identity/Controllers/IdentityControllerBase.cs b/Streetwriters.Identity/Controllers/IdentityControllerBase.cs
index 7c428c7..5c19248 100644
--- a/Streetwriters.Identity/Controllers/IdentityControllerBase.cs
+++ b/Streetwriters.Identity/Controllers/IdentityControllerBase.cs
@@ -36,12 +36,12 @@ namespace Streetwriters.Identity.Controllers
protected UserManager UserManager { get; set; }
protected SignInManager SignInManager { get; set; }
protected RoleManager RoleManager { get; set; }
- protected IEmailSender EmailSender { get; set; }
+ protected ITemplatedEmailSender EmailSender { get; set; }
protected UrlEncoder UrlEncoder { get; set; }
protected IMFAService MFAService { get; set; }
public IdentityControllerBase(
UserManager _userManager,
- IEmailSender _emailSender,
+ ITemplatedEmailSender _emailSender,
SignInManager _signInManager,
RoleManager _roleManager,
IMFAService _mfaService
diff --git a/Streetwriters.Identity/Controllers/MFAController.cs b/Streetwriters.Identity/Controllers/MFAController.cs
index 1dee0e9..b942776 100644
--- a/Streetwriters.Identity/Controllers/MFAController.cs
+++ b/Streetwriters.Identity/Controllers/MFAController.cs
@@ -41,7 +41,7 @@ namespace Streetwriters.Identity.Controllers
[Authorize(LocalApi.PolicyName)]
public class MFAController : IdentityControllerBase
{
- public MFAController(UserManager _userManager, IEmailSender _emailSender,
+ public MFAController(UserManager _userManager, ITemplatedEmailSender _emailSender,
SignInManager _signInManager, RoleManager _roleManager, IMFAService _mfaService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService) { }
[HttpPost]
diff --git a/Streetwriters.Identity/Controllers/SignupController.cs b/Streetwriters.Identity/Controllers/SignupController.cs
index 60cd2f8..f944b9e 100644
--- a/Streetwriters.Identity/Controllers/SignupController.cs
+++ b/Streetwriters.Identity/Controllers/SignupController.cs
@@ -39,7 +39,7 @@ namespace Streetwriters.Identity.Controllers
[Route("signup")]
public class SignupController : IdentityControllerBase
{
- public SignupController(UserManager _userManager, IEmailSender _emailSender,
+ public SignupController(UserManager _userManager, ITemplatedEmailSender _emailSender,
SignInManager _signInManager, RoleManager _roleManager, IMFAService _mfaService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService)
{ }
diff --git a/Streetwriters.Identity/Interfaces/IEmailSender.cs b/Streetwriters.Identity/Interfaces/ITemplatedEmailSender.cs
similarity index 94%
rename from Streetwriters.Identity/Interfaces/IEmailSender.cs
rename to Streetwriters.Identity/Interfaces/ITemplatedEmailSender.cs
index e4ab014..32c9797 100644
--- a/Streetwriters.Identity/Interfaces/IEmailSender.cs
+++ b/Streetwriters.Identity/Interfaces/ITemplatedEmailSender.cs
@@ -1,33 +1,33 @@
-/*
-This file is part of the Notesnook Sync Server project (https://notesnook.com/)
-
-Copyright (C) 2023 Streetwriters (Private) Limited
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the Affero GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-Affero GNU General Public License for more details.
-
-You should have received a copy of the Affero GNU General Public License
-along with this program. If not, see .
-*/
-
-using System.Threading.Tasks;
-using Streetwriters.Common.Interfaces;
-
-namespace Streetwriters.Identity.Interfaces
-{
- public interface IEmailSender
- {
- Task SendConfirmationEmailAsync(string email, string callbackUrl, IClient client);
- Task SendChangeEmailConfirmationAsync(string email, string code, IClient client);
- Task SendPasswordResetEmailAsync(string email, string callbackUrl, IClient client);
- Task Send2FACodeEmailAsync(string email, string code, IClient client);
- Task SendFailedLoginAlertAsync(string email, string deviceInfo, IClient client);
- }
-}
\ No newline at end of file
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.Threading.Tasks;
+using Streetwriters.Common.Interfaces;
+
+namespace Streetwriters.Identity.Interfaces
+{
+ public interface ITemplatedEmailSender
+ {
+ Task SendConfirmationEmailAsync(string email, string callbackUrl, IClient client);
+ Task SendChangeEmailConfirmationAsync(string email, string code, IClient client);
+ Task SendPasswordResetEmailAsync(string email, string callbackUrl, IClient client);
+ Task Send2FACodeEmailAsync(string email, string code, IClient client);
+ Task SendFailedLoginAlertAsync(string email, string deviceInfo, IClient client);
+ }
+}
diff --git a/Streetwriters.Identity/Models/EmailTemplate.cs b/Streetwriters.Identity/Models/EmailTemplate.cs
deleted file mode 100644
index 84d86e9..0000000
--- a/Streetwriters.Identity/Models/EmailTemplate.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
-This file is part of the Notesnook Sync Server project (https://notesnook.com/)
-
-Copyright (C) 2023 Streetwriters (Private) Limited
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the Affero GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-Affero GNU General Public License for more details.
-
-You should have received a copy of the Affero GNU General Public License
-along with this program. If not, see .
-*/
-
-using Streetwriters.Identity.Interfaces;
-
-namespace Streetwriters.Identity.Models
-{
- public class EmailTemplate : IEmailTemplate
- {
- public int? Id { get; set; }
- public object Data { get; set; }
- public long? SendAt { get; set; }
- public string Subject { get; set; }
- public string Html { get; set; }
- public string Text { get; set; }
- }
-}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Services/EmailSender.cs b/Streetwriters.Identity/Services/EmailSender.cs
deleted file mode 100644
index 941dd38..0000000
--- a/Streetwriters.Identity/Services/EmailSender.cs
+++ /dev/null
@@ -1,276 +0,0 @@
-/*
-This file is part of the Notesnook Sync Server project (https://notesnook.com/)
-
-Copyright (C) 2023 Streetwriters (Private) Limited
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the Affero GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-Affero GNU General Public License for more details.
-
-You should have received a copy of the Affero GNU General Public License
-along with this program. If not, see .
-*/
-
-using System.Threading.Tasks;
-using Streetwriters.Identity.Interfaces;
-using SendGrid;
-using SendGrid.Helpers.Mail;
-using Streetwriters.Common;
-using Streetwriters.Common.Interfaces;
-using Streetwriters.Identity.Models;
-using System;
-using System.Net.Http;
-using System.Text.Json;
-using System.Net.Http.Headers;
-using System.Text.Json.Serialization;
-using MailKit.Net.Smtp;
-using MailKit;
-using MimeKit;
-using System.IO;
-using Scriban;
-using WebMarkupMin.Core;
-using WebMarkupMin.Core.Loggers;
-using MimeKit.Cryptography;
-using Org.BouncyCastle.Bcpg.OpenPgp;
-using System.Linq;
-using System.Threading;
-using Org.BouncyCastle.Bcpg;
-using System.Text;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Options;
-
-namespace Streetwriters.Identity.Services
-{
- public class EmailSender : IEmailSender, IAsyncDisposable
- {
- NNGnuPGContext NNGnuPGContext { get; set; }
- SmtpClient mailClient;
- public EmailSender(IConfiguration configuration)
- {
- NNGnuPGContext = new NNGnuPGContext(configuration.GetSection("PgpKeySettings"));
- mailClient = new SmtpClient();
- }
-
- EmailTemplate Email2FATemplate = new EmailTemplate
- {
- Html = ReadMinifiedHtmlFile("Templates/Email2FACode.html"),
- Text = File.ReadAllText("Templates/Email2FACode.txt"),
- Subject = "Your {{app_name}} account 2FA code",
- };
-
- EmailTemplate ConfirmEmailTemplate = new EmailTemplate
- {
- Html = ReadMinifiedHtmlFile("Templates/ConfirmEmail.html"),
- Text = File.ReadAllText("Templates/ConfirmEmail.txt"),
- Subject = "Confirm your {{app_name}} account",
- };
-
- EmailTemplate ConfirmChangeEmailTemplate = new EmailTemplate
- {
- Html = ReadMinifiedHtmlFile("Templates/EmailChangeConfirmation.html"),
- Text = File.ReadAllText("Templates/EmailChangeConfirmation.txt"),
- Subject = "Change {{app_name}} account email address",
- };
-
- EmailTemplate PasswordResetEmailTemplate = new EmailTemplate
- {
- Html = ReadMinifiedHtmlFile("Templates/ResetAccountPassword.html"),
- Text = File.ReadAllText("Templates/ResetAccountPassword.txt"),
- Subject = "Reset {{app_name}} account password",
- };
-
- EmailTemplate FailedLoginAlertTemplate = new EmailTemplate
- {
- Html = ReadMinifiedHtmlFile("Templates/FailedLoginAlert.html"),
- Text = File.ReadAllText("Templates/FailedLoginAlert.txt"),
- Subject = "Failed login attempt on your {{app_name}} account",
- };
-
- public async Task Send2FACodeEmailAsync(string email, string code, IClient client)
- {
- var template = new EmailTemplate
- {
- Html = Email2FATemplate.Html,
- Text = Email2FATemplate.Text,
- Subject = Email2FATemplate.Subject,
- Data = new
- {
- app_name = client.Name,
- code = code
- }
- };
- await SendEmailAsync(email, template, client);
- }
-
- public async Task SendConfirmationEmailAsync(string email, string callbackUrl, IClient client)
- {
- var template = new EmailTemplate
- {
- Html = ConfirmEmailTemplate.Html,
- Text = ConfirmEmailTemplate.Text,
- Subject = ConfirmEmailTemplate.Subject,
- Data = new
- {
- app_name = client.Name,
- confirm_link = callbackUrl
- }
- };
- await SendEmailAsync(email, template, client);
- }
-
- public async Task SendChangeEmailConfirmationAsync(string email, string code, IClient client)
- {
- var template = new EmailTemplate
- {
- Html = ConfirmChangeEmailTemplate.Html,
- Text = ConfirmChangeEmailTemplate.Text,
- Subject = ConfirmChangeEmailTemplate.Subject,
- Data = new
- {
- app_name = client.Name,
- code = code
- }
- };
- await SendEmailAsync(email, template, client);
- }
-
- public async Task SendPasswordResetEmailAsync(string email, string callbackUrl, IClient client)
- {
- var template = new EmailTemplate
- {
- Html = PasswordResetEmailTemplate.Html,
- Text = PasswordResetEmailTemplate.Text,
- Subject = PasswordResetEmailTemplate.Subject,
- Data = new
- {
- app_name = client.Name,
- reset_link = callbackUrl
- }
- };
- await SendEmailAsync(email, template, client);
- }
-
-
- public async Task SendFailedLoginAlertAsync(string email, string deviceInfo, IClient client)
- {
- var template = new EmailTemplate
- {
- Html = FailedLoginAlertTemplate.Html,
- Text = FailedLoginAlertTemplate.Text,
- Subject = FailedLoginAlertTemplate.Subject,
- Data = new
- {
- app_name = client.Name,
- device_info = deviceInfo.Replace("\n", "
")
- }
- };
- await SendEmailAsync(email, template, client);
- }
-
- private async Task SendEmailAsync(string email, IEmailTemplate template, IClient client)
- {
- if (!mailClient.IsConnected)
- {
- if (int.TryParse(Constants.SMTP_PORT, out int port))
- {
- await mailClient.ConnectAsync(Constants.SMTP_HOST, port, MailKit.Security.SecureSocketOptions.Auto);
- }
- else
- {
- throw new InvalidDataException("SMTP_PORT is not a valid integer value.");
- }
- }
-
- if (!mailClient.IsAuthenticated)
- await mailClient.AuthenticateAsync(Constants.SMTP_USERNAME, Constants.SMTP_PASSWORD);
-
- var message = new MimeMessage();
- var sender = new MailboxAddress(client.SenderName, client.SenderEmail);
- message.From.Add(sender);
- message.To.Add(new MailboxAddress("", email));
- message.Subject = await Template.Parse(template.Subject).RenderAsync(template.Data);
-
- if (!string.IsNullOrEmpty(Constants.SMTP_REPLYTO_EMAIL))
- message.ReplyTo.Add(MailboxAddress.Parse(Constants.SMTP_REPLYTO_EMAIL));
-
- message.Body = await GetEmailBodyAsync(template, client, sender);
-
- await mailClient.SendAsync(message);
- }
-
- private async Task GetEmailBodyAsync(IEmailTemplate template, IClient client, MailboxAddress sender)
- {
- var builder = new BodyBuilder();
- try
- {
- builder.TextBody = await Template.Parse(template.Text).RenderAsync(template.Data);
- builder.HtmlBody = await Template.Parse(template.Html).RenderAsync(template.Data);
-
- var key = NNGnuPGContext.GetSigningKey(sender);
- if (key != null)
- {
- using (MemoryStream outputStream = new MemoryStream())
- {
- using (Stream armoredStream = new ArmoredOutputStream(outputStream))
- {
- key.PublicKey.Encode(armoredStream);
- }
- outputStream.Seek(0, SeekOrigin.Begin);
- builder.Attachments.Add($"{client.Id}_pub.asc", Encoding.ASCII.GetBytes(Encoding.ASCII.GetString(outputStream.ToArray())));
- }
- return await MultipartSigned.CreateAsync(NNGnuPGContext, sender, DigestAlgorithm.Sha256, builder.ToMessageBody());
- }
- else
- {
- return builder.ToMessageBody();
- }
- }
- catch (Exception ex)
- {
- await Slogger.Error("GetEmailBodyAsync", ex.ToString());
- return builder.ToMessageBody();
- }
- }
-
- async ValueTask IAsyncDisposable.DisposeAsync()
- {
- await mailClient.DisconnectAsync(true);
- mailClient.Dispose();
- }
-
-
- static string ReadMinifiedHtmlFile(string path)
- {
- var settings = new HtmlMinificationSettings()
- {
- WhitespaceMinificationMode = WhitespaceMinificationMode.Medium
- };
- var cssMinifier = new KristensenCssMinifier();
- var jsMinifier = new CrockfordJsMinifier();
-
- var minifier = new HtmlMinifier(settings, cssMinifier, jsMinifier, new NullLogger());
-
- return minifier.Minify(File.ReadAllText(path), false).MinifiedContent;
- }
- }
-
- class NNGnuPGContext : GnuPGContext
- {
- IConfiguration PgpKeySettings { get; set; }
- public NNGnuPGContext(IConfiguration pgpKeySettings)
- {
- PgpKeySettings = pgpKeySettings;
- }
-
- protected override string GetPasswordForKey(PgpSecretKey key)
- {
- return PgpKeySettings[key.KeyId.ToString("X")];
- }
- }
-}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Services/MFAService.cs b/Streetwriters.Identity/Services/MFAService.cs
index b37edda..b0ddeb4 100644
--- a/Streetwriters.Identity/Services/MFAService.cs
+++ b/Streetwriters.Identity/Services/MFAService.cs
@@ -39,9 +39,9 @@ namespace Streetwriters.Identity.Services
const string SMS_ID_CLAIM = "mfa:sms:id";
private UserManager UserManager { get; set; }
- private IEmailSender EmailSender { get; set; }
+ private ITemplatedEmailSender EmailSender { get; set; }
private ISMSSender SMSSender { get; set; }
- public MFAService(UserManager _userManager, IEmailSender emailSender, ISMSSender smsSender)
+ public MFAService(UserManager _userManager, ITemplatedEmailSender emailSender, ISMSSender smsSender)
{
UserManager = _userManager;
EmailSender = emailSender;
diff --git a/Streetwriters.Identity/Services/TemplatedEmailSender.cs b/Streetwriters.Identity/Services/TemplatedEmailSender.cs
new file mode 100644
index 0000000..f7f5f96
--- /dev/null
+++ b/Streetwriters.Identity/Services/TemplatedEmailSender.cs
@@ -0,0 +1,184 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using MailKit;
+using MailKit.Net.Smtp;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+using MimeKit;
+using MimeKit.Cryptography;
+using Org.BouncyCastle.Bcpg;
+using Org.BouncyCastle.Bcpg.OpenPgp;
+using Scriban;
+using SendGrid;
+using SendGrid.Helpers.Mail;
+using Streetwriters.Common;
+using Streetwriters.Common.Helpers;
+using Streetwriters.Common.Interfaces;
+using Streetwriters.Common.Models;
+using Streetwriters.Identity.Interfaces;
+using Streetwriters.Identity.Models;
+using WebMarkupMin.Core;
+using WebMarkupMin.Core.Loggers;
+
+namespace Streetwriters.Identity.Services
+{
+ public class TemplatedEmailSender : ITemplatedEmailSender
+ {
+ NNGnuPGContext NNGnuPGContext { get; set; }
+ IEmailSender EmailSender { get; set; }
+
+ public TemplatedEmailSender(IConfiguration configuration, IEmailSender emailSender)
+ {
+ NNGnuPGContext = new NNGnuPGContext(configuration.GetSection("PgpKeySettings"));
+ EmailSender = emailSender;
+ }
+
+ EmailTemplate Email2FATemplate = new EmailTemplate
+ {
+ Html = HtmlHelper.ReadMinifiedHtmlFile("Templates/Email2FACode.html"),
+ Text = File.ReadAllText("Templates/Email2FACode.txt"),
+ Subject = "Your {{app_name}} account 2FA code",
+ };
+
+ EmailTemplate ConfirmEmailTemplate = new EmailTemplate
+ {
+ Html = HtmlHelper.ReadMinifiedHtmlFile("Templates/ConfirmEmail.html"),
+ Text = File.ReadAllText("Templates/ConfirmEmail.txt"),
+ Subject = "Confirm your {{app_name}} account",
+ };
+
+ EmailTemplate ConfirmChangeEmailTemplate = new EmailTemplate
+ {
+ Html = HtmlHelper.ReadMinifiedHtmlFile("Templates/EmailChangeConfirmation.html"),
+ Text = File.ReadAllText("Templates/EmailChangeConfirmation.txt"),
+ Subject = "Change {{app_name}} account email address",
+ };
+
+ EmailTemplate PasswordResetEmailTemplate = new EmailTemplate
+ {
+ Html = HtmlHelper.ReadMinifiedHtmlFile("Templates/ResetAccountPassword.html"),
+ Text = File.ReadAllText("Templates/ResetAccountPassword.txt"),
+ Subject = "Reset {{app_name}} account password",
+ };
+
+ EmailTemplate FailedLoginAlertTemplate = new EmailTemplate
+ {
+ Html = HtmlHelper.ReadMinifiedHtmlFile("Templates/FailedLoginAlert.html"),
+ Text = File.ReadAllText("Templates/FailedLoginAlert.txt"),
+ Subject = "Failed login attempt on your {{app_name}} account",
+ };
+
+ public async Task Send2FACodeEmailAsync(string email, string code, IClient client)
+ {
+ var template = new EmailTemplate
+ {
+ Html = Email2FATemplate.Html,
+ Text = Email2FATemplate.Text,
+ Subject = Email2FATemplate.Subject,
+ Data = new { app_name = client.Name, code = code },
+ };
+ await EmailSender.SendEmailAsync(email, template, client, NNGnuPGContext);
+ }
+
+ public async Task SendConfirmationEmailAsync(
+ string email,
+ string callbackUrl,
+ IClient client
+ )
+ {
+ var template = new EmailTemplate
+ {
+ Html = ConfirmEmailTemplate.Html,
+ Text = ConfirmEmailTemplate.Text,
+ Subject = ConfirmEmailTemplate.Subject,
+ Data = new { app_name = client.Name, confirm_link = callbackUrl },
+ };
+ await EmailSender.SendEmailAsync(email, template, client, NNGnuPGContext);
+ }
+
+ public async Task SendChangeEmailConfirmationAsync(
+ string email,
+ string code,
+ IClient client
+ )
+ {
+ var template = new EmailTemplate
+ {
+ Html = ConfirmChangeEmailTemplate.Html,
+ Text = ConfirmChangeEmailTemplate.Text,
+ Subject = ConfirmChangeEmailTemplate.Subject,
+ Data = new { app_name = client.Name, code = code },
+ };
+ await EmailSender.SendEmailAsync(email, template, client, NNGnuPGContext);
+ }
+
+ public async Task SendPasswordResetEmailAsync(
+ string email,
+ string callbackUrl,
+ IClient client
+ )
+ {
+ var template = new EmailTemplate
+ {
+ Html = PasswordResetEmailTemplate.Html,
+ Text = PasswordResetEmailTemplate.Text,
+ Subject = PasswordResetEmailTemplate.Subject,
+ Data = new { app_name = client.Name, reset_link = callbackUrl },
+ };
+ await EmailSender.SendEmailAsync(email, template, client, NNGnuPGContext);
+ }
+
+ public async Task SendFailedLoginAlertAsync(string email, string deviceInfo, IClient client)
+ {
+ var template = new EmailTemplate
+ {
+ Html = FailedLoginAlertTemplate.Html,
+ Text = FailedLoginAlertTemplate.Text,
+ Subject = FailedLoginAlertTemplate.Subject,
+ Data = new
+ {
+ app_name = client.Name,
+ device_info = deviceInfo.Replace("\n", "
"),
+ },
+ };
+ await EmailSender.SendEmailAsync(email, template, client, NNGnuPGContext);
+ }
+ }
+
+ public class NNGnuPGContext(IConfiguration pgpKeySettings) : GnuPGContext
+ {
+ IConfiguration PgpKeySettings { get; set; } = pgpKeySettings;
+
+ protected override string GetPasswordForKey(PgpSecretKey key)
+ {
+ return PgpKeySettings[key.KeyId.ToString("X")];
+ }
+ }
+}
diff --git a/Streetwriters.Identity/Startup.cs b/Streetwriters.Identity/Startup.cs
index 5b3b2ac..fc32053 100644
--- a/Streetwriters.Identity/Startup.cs
+++ b/Streetwriters.Identity/Startup.cs
@@ -43,6 +43,7 @@ using Streetwriters.Common.Extensions;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Messages;
using Streetwriters.Common.Models;
+using Streetwriters.Common.Services;
using Streetwriters.Identity.Helpers;
using Streetwriters.Identity.Interfaces;
using Streetwriters.Identity.Jobs;
@@ -69,6 +70,7 @@ namespace Streetwriters.Identity
var connectionString = Constants.MONGODB_CONNECTION_STRING;
services.AddTransient();
+ services.AddTransient();
services.AddTransient();
services.AddTransient, Argon2PasswordHasher>();
diff --git a/Streetwriters.Identity/Streetwriters.Identity.csproj b/Streetwriters.Identity/Streetwriters.Identity.csproj
index 5261bf7..58fedd7 100644
--- a/Streetwriters.Identity/Streetwriters.Identity.csproj
+++ b/Streetwriters.Identity/Streetwriters.Identity.csproj
@@ -8,12 +8,11 @@
-
+
-
@@ -27,8 +26,6 @@
-
-
diff --git a/Streetwriters.Identity/Validation/MFAGrantValidator.cs b/Streetwriters.Identity/Validation/MFAGrantValidator.cs
index 9d893f4..788f320 100644
--- a/Streetwriters.Identity/Validation/MFAGrantValidator.cs
+++ b/Streetwriters.Identity/Validation/MFAGrantValidator.cs
@@ -48,8 +48,8 @@ namespace Streetwriters.Identity.Validation
private IHttpContextAccessor HttpContextAccessor { get; set; }
private ITokenValidator TokenValidator { get; set; }
private ITokenGenerationService TokenGenerationService { get; set; }
- private IEmailSender EmailSender { get; set; }
- public MFAGrantValidator(UserManager userManager, SignInManager signInManager, IMFAService mfaService, IHttpContextAccessor httpContextAccessor, ITokenValidator tokenValidator, ITokenGenerationService tokenGenerationService, IEmailSender emailSender)
+ private ITemplatedEmailSender EmailSender { get; set; }
+ public MFAGrantValidator(UserManager userManager, SignInManager signInManager, IMFAService mfaService, IHttpContextAccessor httpContextAccessor, ITokenValidator tokenValidator, ITokenGenerationService tokenGenerationService, ITemplatedEmailSender emailSender)
{
UserManager = userManager;
SignInManager = signInManager;
diff --git a/Streetwriters.Identity/Validation/MFAPasswordGrantValidator.cs b/Streetwriters.Identity/Validation/MFAPasswordGrantValidator.cs
index ab8fcd9..9952954 100644
--- a/Streetwriters.Identity/Validation/MFAPasswordGrantValidator.cs
+++ b/Streetwriters.Identity/Validation/MFAPasswordGrantValidator.cs
@@ -40,9 +40,9 @@ namespace Streetwriters.Identity.Validation
private IMFAService MFAService { get; set; }
private IHttpContextAccessor HttpContextAccessor { get; set; }
private ITokenValidator TokenValidator { get; set; }
- private IEmailSender EmailSender { get; set; }
+ private ITemplatedEmailSender EmailSender { get; set; }
- public MFAPasswordGrantValidator(UserManager userManager, SignInManager signInManager, IMFAService mfaService, IHttpContextAccessor httpContextAccessor, ITokenValidator tokenValidator, IEmailSender emailSender)
+ public MFAPasswordGrantValidator(UserManager userManager, SignInManager signInManager, IMFAService mfaService, IHttpContextAccessor httpContextAccessor, ITokenValidator tokenValidator, ITemplatedEmailSender emailSender)
{
UserManager = userManager;
SignInManager = signInManager;