/*
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")];
}
}
}