mirror of
https://github.com/streetwriters/notesnook-sync-server.git
synced 2026-02-12 11:12:44 +00:00
identity: move email sender to common
This commit is contained in:
23
Streetwriters.Common/Helpers/HtmlHelper.cs
Normal file
23
Streetwriters.Common/Helpers/HtmlHelper.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Streetwriters.Common/Interfaces/IEmailSender.cs
Normal file
19
Streetwriters.Common/Interfaces/IEmailSender.cs
Normal file
@@ -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<string, byte[]> attachments = null
|
||||
);
|
||||
}
|
||||
}
|
||||
11
Streetwriters.Common/Models/EmailTemplate.cs
Normal file
11
Streetwriters.Common/Models/EmailTemplate.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
134
Streetwriters.Common/Services/EmailSender.cs
Normal file
134
Streetwriters.Common/Services/EmailSender.cs
Normal file
@@ -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<string, byte[]> 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<MimeEntity> GetEmailBodyAsync(
|
||||
EmailTemplate template,
|
||||
IClient client,
|
||||
MailboxAddress sender,
|
||||
GnuPGContext gpgContext = null,
|
||||
Dictionary<string, byte[]> 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<EmailSender>.Error("GetEmailBodyAsync", ex.ToString());
|
||||
return builder.ToMessageBody();
|
||||
}
|
||||
}
|
||||
|
||||
async ValueTask IAsyncDisposable.DisposeAsync()
|
||||
{
|
||||
await mailClient.DisconnectAsync(true);
|
||||
mailClient.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,16 @@
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MailKit" Version="4.9.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Cors" Version="2.2.0" />
|
||||
<PackageReference Include="Scriban" Version="5.12.1" />
|
||||
<PackageReference Include="WampSharp.Default" Version="20.1.1" />
|
||||
<PackageReference Include="WampSharp.AspNetCore.WebSockets.Server" Version="20.1.1" />
|
||||
<PackageReference Include="WampSharp.NewtonsoftMsgpack" Version="20.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
|
||||
<PackageReference Include="AspNetCore.Identity.Mongo" Version="8.3.3" />
|
||||
<PackageReference Include="WebMarkupMin.NUglify" Version="2.18.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
@@ -52,10 +52,8 @@ namespace Streetwriters.Identity.Controllers
|
||||
{
|
||||
private IPersistedGrantStore PersistedGrantStore { get; set; }
|
||||
private ITokenGenerationService TokenGenerationService { get; set; }
|
||||
private IUserClaimsPrincipalFactory<User> PrincipalFactory { get; set; }
|
||||
private IdentityServerOptions ISOptions { get; set; }
|
||||
private IUserAccountService UserAccountService { get; set; }
|
||||
public AccountController(UserManager<User> _userManager, IEmailSender _emailSender,
|
||||
public AccountController(UserManager<User> _userManager, ITemplatedEmailSender _emailSender,
|
||||
SignInManager<User> _signInManager, RoleManager<MongoRole> _roleManager, IPersistedGrantStore store,
|
||||
ITokenGenerationService tokenGenerationService, IMFAService _mfaService, IUserAccountService userAccountService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService)
|
||||
{
|
||||
@@ -341,4 +339,4 @@ namespace Streetwriters.Identity.Controllers
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,12 +36,12 @@ namespace Streetwriters.Identity.Controllers
|
||||
protected UserManager<User> UserManager { get; set; }
|
||||
protected SignInManager<User> SignInManager { get; set; }
|
||||
protected RoleManager<MongoRole> 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<User> _userManager,
|
||||
IEmailSender _emailSender,
|
||||
ITemplatedEmailSender _emailSender,
|
||||
SignInManager<User> _signInManager,
|
||||
RoleManager<MongoRole> _roleManager,
|
||||
IMFAService _mfaService
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Streetwriters.Identity.Controllers
|
||||
[Authorize(LocalApi.PolicyName)]
|
||||
public class MFAController : IdentityControllerBase
|
||||
{
|
||||
public MFAController(UserManager<User> _userManager, IEmailSender _emailSender,
|
||||
public MFAController(UserManager<User> _userManager, ITemplatedEmailSender _emailSender,
|
||||
SignInManager<User> _signInManager, RoleManager<MongoRole> _roleManager, IMFAService _mfaService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService) { }
|
||||
|
||||
[HttpPost]
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace Streetwriters.Identity.Controllers
|
||||
[Route("signup")]
|
||||
public class SignupController : IdentityControllerBase
|
||||
{
|
||||
public SignupController(UserManager<User> _userManager, IEmailSender _emailSender,
|
||||
public SignupController(UserManager<User> _userManager, ITemplatedEmailSender _emailSender,
|
||||
SignInManager<User> _signInManager, RoleManager<MongoRole> _roleManager, IMFAService _mfaService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService)
|
||||
{ }
|
||||
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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", "<br>")
|
||||
}
|
||||
};
|
||||
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<MimeEntity> 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<EmailSender>.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")];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,9 +39,9 @@ namespace Streetwriters.Identity.Services
|
||||
const string SMS_ID_CLAIM = "mfa:sms:id";
|
||||
|
||||
private UserManager<User> UserManager { get; set; }
|
||||
private IEmailSender EmailSender { get; set; }
|
||||
private ITemplatedEmailSender EmailSender { get; set; }
|
||||
private ISMSSender SMSSender { get; set; }
|
||||
public MFAService(UserManager<User> _userManager, IEmailSender emailSender, ISMSSender smsSender)
|
||||
public MFAService(UserManager<User> _userManager, ITemplatedEmailSender emailSender, ISMSSender smsSender)
|
||||
{
|
||||
UserManager = _userManager;
|
||||
EmailSender = emailSender;
|
||||
|
||||
184
Streetwriters.Identity/Services/TemplatedEmailSender.cs
Normal file
184
Streetwriters.Identity/Services/TemplatedEmailSender.cs
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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", "<br>"),
|
||||
},
|
||||
};
|
||||
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")];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<IEmailSender, EmailSender>();
|
||||
services.AddTransient<ITemplatedEmailSender, TemplatedEmailSender>();
|
||||
services.AddTransient<ISMSSender, SMSSender>();
|
||||
services.AddTransient<IPasswordHasher<User>, Argon2PasswordHasher<User>>();
|
||||
|
||||
|
||||
@@ -8,12 +8,11 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DotNetEnv" Version="2.3.0" />
|
||||
<PackageReference Include="IdentityServer4" Version="4.1.2" />
|
||||
<PackageReference Include="MailKit" Version="3.4.3" />
|
||||
<PackageReference Include="MailKit" Version="4.9.0" />
|
||||
<PackageReference Include="MessageBird" Version="3.2.0" />
|
||||
<PackageReference Include="Ng.UserAgentService" Version="1.1.2" />
|
||||
<PackageReference Include="Quartz" Version="3.5.0" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.5.0" />
|
||||
<PackageReference Include="Scriban" Version="5.5.1" />
|
||||
<PackageReference Include="SendGrid" Version="9.24.4" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
|
||||
<PackageReference Include="Geralt" Version="3.1.0" />
|
||||
@@ -27,8 +26,6 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="7.0.4" />
|
||||
<PackageReference Include="AspNetCore.Identity.Mongo" Version="8.3.3" />
|
||||
<PackageReference Include="Twilio" Version="6.13.0" />
|
||||
<PackageReference Include="WebMarkupMin.Core" Version="2.13.0" />
|
||||
<PackageReference Include="WebMarkupMin.NUglify" Version="2.12.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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<User> userManager, SignInManager<User> signInManager, IMFAService mfaService, IHttpContextAccessor httpContextAccessor, ITokenValidator tokenValidator, ITokenGenerationService tokenGenerationService, IEmailSender emailSender)
|
||||
private ITemplatedEmailSender EmailSender { get; set; }
|
||||
public MFAGrantValidator(UserManager<User> userManager, SignInManager<User> signInManager, IMFAService mfaService, IHttpContextAccessor httpContextAccessor, ITokenValidator tokenValidator, ITokenGenerationService tokenGenerationService, ITemplatedEmailSender emailSender)
|
||||
{
|
||||
UserManager = userManager;
|
||||
SignInManager = signInManager;
|
||||
|
||||
@@ -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<User> userManager, SignInManager<User> signInManager, IMFAService mfaService, IHttpContextAccessor httpContextAccessor, ITokenValidator tokenValidator, IEmailSender emailSender)
|
||||
public MFAPasswordGrantValidator(UserManager<User> userManager, SignInManager<User> signInManager, IMFAService mfaService, IHttpContextAccessor httpContextAccessor, ITokenValidator tokenValidator, ITemplatedEmailSender emailSender)
|
||||
{
|
||||
UserManager = userManager;
|
||||
SignInManager = signInManager;
|
||||
|
||||
Reference in New Issue
Block a user