/* This file is part of the Notesnook Sync Server project (https://notesnook.com/) Copyright (C) 2022 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 { IOptions SmtpOptions { get; set; } NNGnuPGContext NNGnuPGContext { get; set; } public EmailSender(IConfiguration configuration, IOptions smtpOptions) { SmtpOptions = smtpOptions; NNGnuPGContext = new NNGnuPGContext(configuration.GetSection("PgpKeySettings")); } 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", }; SmtpClient mailClient; public EmailSender() { mailClient = new SmtpClient(); } 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 Task SendWelcomeEmailAsync(string email, IClient client) { // EmailTemplate template = new EmailTemplate // { // Id = client.WelcomeEmailTemplateId, // Data = new { }, // SendAt = DateTimeOffset.UtcNow.AddHours(2).ToUnixTimeSeconds() // }; // return SendEmailAsync(email, template, client); return Task.CompletedTask; } 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 callbackUrl, IClient client) { var template = new EmailTemplate { Html = ConfirmChangeEmailTemplate.Html, Text = ConfirmChangeEmailTemplate.Text, Subject = ConfirmChangeEmailTemplate.Subject, Data = new { app_name = client.Name, confirm_link = callbackUrl } }; 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) { try { if (!mailClient.IsConnected) await mailClient.ConnectAsync(SmtpOptions.Value.Host, SmtpOptions.Value.Port, MailKit.Security.SecureSocketOptions.StartTls); if (!mailClient.IsAuthenticated) await mailClient.AuthenticateAsync(SmtpOptions.Value.Username, SmtpOptions.Value.Password); var message = new MimeMessage(); var sender = new MailboxAddress(client.SenderName, client.SenderEmail); message.From.Add(sender); message.To.Add(new MailboxAddress("", email)); message.ReplyTo.Add(new MailboxAddress("Streetwriters", "support@streetwriters.co")); message.Subject = await Template.Parse(template.Subject).RenderAsync(template.Data); var builder = new BodyBuilder(); 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()))); } } message.Body = MultipartSigned.Create(NNGnuPGContext, sender, DigestAlgorithm.Sha256, builder.ToMessageBody()); await mailClient.SendAsync(message); } catch (Exception ex) { Console.Error.WriteLine(ex.Message); } } 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")]; } } }