Added new E-Mail Api
This commit is contained in:
parent
42137402e2
commit
37ffb49ca1
|
@ -7,5 +7,6 @@ public class SmtpConfiguration {
|
||||||
public required string Password { get; init; }
|
public required string Password { get; init; }
|
||||||
public required string SenderEmail { get; init; }
|
public required string SenderEmail { get; init; }
|
||||||
public required string SenderName { get; init; }
|
public required string SenderName { get; init; }
|
||||||
|
public required string ServiceEmail { get; init; }
|
||||||
public bool Ssl { get; init; } = true;
|
public bool Ssl { get; init; } = true;
|
||||||
}
|
}
|
|
@ -124,6 +124,7 @@
|
||||||
var smtpConfig = builder.Configuration.GetSection("Email:Smtp");
|
var smtpConfig = builder.Configuration.GetSection("Email:Smtp");
|
||||||
if (smtpConfig.Exists()) {
|
if (smtpConfig.Exists()) {
|
||||||
builder.Services.Configure<SmtpConfiguration>(smtpConfig);
|
builder.Services.Configure<SmtpConfiguration>(smtpConfig);
|
||||||
|
builder.Services.AddKeyedScoped<IEmailService, LiveEmailService>("live");
|
||||||
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
|
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
|
||||||
builder.Services.AddScoped<IAdvancedEmailSender, SmtpEmailSender>();
|
builder.Services.AddScoped<IAdvancedEmailSender, SmtpEmailSender>();
|
||||||
builder.Services.AddScoped<IEmailSender<ApplicationUser>, SmtpEmailSender>();
|
builder.Services.AddScoped<IEmailSender<ApplicationUser>, SmtpEmailSender>();
|
||||||
|
@ -132,6 +133,8 @@
|
||||||
logMessages.Add("No Email provider configured.");
|
logMessages.Add("No Email provider configured.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
builder.Services.AddScoped<EmailFactory>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<IMessageDisplay, MessageService>();
|
builder.Services.AddSingleton<IMessageDisplay, MessageService>();
|
||||||
builder.Services.AddSingleton<FileSystemService>();
|
builder.Services.AddSingleton<FileSystemService>();
|
||||||
builder.Services.AddSingleton<EmailTemplateService>();
|
builder.Services.AddSingleton<EmailTemplateService>();
|
||||||
|
|
67
Wave/Services/EmailFactory.cs
Normal file
67
Wave/Services/EmailFactory.cs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using MimeKit;
|
||||||
|
using Wave.Data;
|
||||||
|
using Wave.Utilities;
|
||||||
|
|
||||||
|
namespace Wave.Services;
|
||||||
|
|
||||||
|
public class EmailFactory(IOptions<Customization> customizations, EmailTemplateService templateService) {
|
||||||
|
private Customization Customizations { get; } = customizations.Value;
|
||||||
|
private EmailTemplateService TemplateService { get; } = templateService;
|
||||||
|
|
||||||
|
public async ValueTask<IEmail> CreateDefaultEmail(string receiverMail, string? receiverName, string subject, string title, string bodyHtml) {
|
||||||
|
(string host, string logo) = GetStaticData();
|
||||||
|
string body = await TemplateService.DefaultAsync(host, logo, title, bodyHtml);
|
||||||
|
|
||||||
|
return new StaticEmail(receiverMail, receiverName, subject, title, body, FrozenDictionary<string, string>.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<IEmail> CreateSubscribedEmail(EmailSubscriber subscriber, string browserLink, string subject, string title, string bodyHtml, string role = "unknown") {
|
||||||
|
(string host, string logo) = GetStaticData();
|
||||||
|
|
||||||
|
string unsubscribeLink = await GetUnsubscribeLink(host, subscriber.Id, role);
|
||||||
|
string body = await TemplateService.NewsletterAsync(host, browserLink, logo, title, bodyHtml, unsubscribeLink);
|
||||||
|
|
||||||
|
return new StaticEmail(subscriber.Email, subscriber.Name, subject, title, body, new Dictionary<string, string>{
|
||||||
|
{HeaderId.ListUnsubscribe.ToHeaderName(), $"<{unsubscribeLink}>"},
|
||||||
|
{HeaderId.ListUnsubscribePost.ToHeaderName(), "One-Click"}
|
||||||
|
}.ToFrozenDictionary());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<IEmail> CreateWelcomeEmail(EmailSubscriber subscriber, IEnumerable<EmailNewsletter> articles, string subject, string title, string bodyHtml) {
|
||||||
|
(string host, string logo) = GetStaticData();
|
||||||
|
|
||||||
|
string articlePartial = await TemplateService.GetPartialAsync("email-article");
|
||||||
|
var articlesHtml = new StringBuilder("");
|
||||||
|
foreach (var n in articles) {
|
||||||
|
string articleLink = ArticleUtilities.GenerateArticleLink(n.Article, new Uri(Customizations.AppUrl, UriKind.Absolute));
|
||||||
|
articlesHtml.AppendFormat(
|
||||||
|
articlePartial,
|
||||||
|
n.Article.Title, n.Article.Author.Name, n.Article.Body[..Math.Min(250, n.Article.Body.Length)], articleLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
string unsubscribeLink = await GetUnsubscribeLink(host, subscriber.Id, "welcome");
|
||||||
|
string body = TemplateService.Welcome(host, logo, title, bodyHtml, unsubscribeLink, articlesHtml.ToString());
|
||||||
|
|
||||||
|
return new StaticEmail(subscriber.Email, subscriber.Name, subject, title, body, new Dictionary<string, string>{
|
||||||
|
{HeaderId.ListUnsubscribe.ToHeaderName(), $"<{unsubscribeLink}>"},
|
||||||
|
{HeaderId.ListUnsubscribePost.ToHeaderName(), "One-Click"}
|
||||||
|
}.ToFrozenDictionary());
|
||||||
|
}
|
||||||
|
|
||||||
|
private (string host, string logo) GetStaticData() {
|
||||||
|
var host = new Uri(string.IsNullOrWhiteSpace(Customizations.AppUrl) ? "" : Customizations.AppUrl); // TODO get link
|
||||||
|
string logo = !string.IsNullOrWhiteSpace(Customizations.LogoLink)
|
||||||
|
? Customizations.LogoLink
|
||||||
|
: new Uri(host, "/img/logo.png").AbsoluteUri;
|
||||||
|
return (host.AbsoluteUri, logo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<string> GetUnsubscribeLink(string host, Guid subscriberId, string role) {
|
||||||
|
(string user, string token) = await TemplateService.CreateConfirmTokensAsync(subscriberId, "unsubscribe-"+role, TimeSpan.FromDays(30));
|
||||||
|
return new Uri(new Uri(host), $"/Email/Unsubscribe?newsletter={role}&user={WebUtility.UrlEncode(user)}&token={WebUtility.UrlEncode(token)}").AbsoluteUri;
|
||||||
|
}
|
||||||
|
}
|
3
Wave/Services/EmailNotSendException.cs
Normal file
3
Wave/Services/EmailNotSendException.cs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
namespace Wave.Services;
|
||||||
|
|
||||||
|
public class EmailNotSendException(string message, Exception exception) : ApplicationException(message, exception);
|
|
@ -51,6 +51,15 @@ public enum Constants {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ValueTask<string> DefaultAsync(string home, string logoLink, string title, string body) {
|
||||||
|
return ProcessAsync("default", new Dictionary<Constants, object?> {
|
||||||
|
{Constants.HomeLink, home},
|
||||||
|
{Constants.ContentLogo, logoLink},
|
||||||
|
{Constants.ContentTitle, title},
|
||||||
|
{Constants.ContentBody, body}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public string Newsletter(string home, string browserUrl, string logoLink, string title, string body, string unsubscribe) {
|
public string Newsletter(string home, string browserUrl, string logoLink, string title, string body, string unsubscribe) {
|
||||||
return Process("newsletter", new Dictionary<Constants, object?> {
|
return Process("newsletter", new Dictionary<Constants, object?> {
|
||||||
{ Constants.HomeLink, home },
|
{ Constants.HomeLink, home },
|
||||||
|
@ -61,6 +70,16 @@ public enum Constants {
|
||||||
{ Constants.EmailUnsubscribeLink, unsubscribe }
|
{ Constants.EmailUnsubscribeLink, unsubscribe }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
public ValueTask<string> NewsletterAsync(string home, string browserUrl, string logoLink, string title, string body, string unsubscribe) {
|
||||||
|
return ProcessAsync("newsletter", new Dictionary<Constants, object?> {
|
||||||
|
{ Constants.HomeLink, home },
|
||||||
|
{ Constants.BrowserLink, browserUrl },
|
||||||
|
{ Constants.ContentLogo, logoLink },
|
||||||
|
{ Constants.ContentTitle, title },
|
||||||
|
{ Constants.ContentBody, body },
|
||||||
|
{ Constants.EmailUnsubscribeLink, unsubscribe }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public string Welcome(string home, string logoLink, string title, string body, string unsubscribe, string articles) {
|
public string Welcome(string home, string logoLink, string title, string body, string unsubscribe, string articles) {
|
||||||
return Process("welcome", new Dictionary<Constants, object?> {
|
return Process("welcome", new Dictionary<Constants, object?> {
|
||||||
|
@ -77,15 +96,7 @@ public enum Constants {
|
||||||
FileSystem.GetEmailTemplate("default", DefaultTemplates["default"]);
|
FileSystem.GetEmailTemplate("default", DefaultTemplates["default"]);
|
||||||
FileSystem.GetEmailTemplate("newsletter", DefaultTemplates["newsletter"]);
|
FileSystem.GetEmailTemplate("newsletter", DefaultTemplates["newsletter"]);
|
||||||
FileSystem.GetEmailTemplate("welcome", DefaultTemplates["welcome"]);
|
FileSystem.GetEmailTemplate("welcome", DefaultTemplates["welcome"]);
|
||||||
FileSystem.GetPartialTemplateAsync("email-article",
|
FileSystem.GetPartialTemplate("email-article", DefaultPartials["email-article"]);
|
||||||
"""
|
|
||||||
<div style="padding: 10px; background: #9f9f9f; color: #fff; margin-bottom: 10px; border-radius: 2px">
|
|
||||||
<h3 style="margin-top: 0;">{0}</h3>
|
|
||||||
<small>{1}</small>
|
|
||||||
<p>{2}...</p>
|
|
||||||
<a href="{3}">Link</a>
|
|
||||||
</div>
|
|
||||||
""");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ApplyTokens(string template, Func<string, string?> replacer) {
|
public string ApplyTokens(string template, Func<string, string?> replacer) {
|
||||||
|
@ -97,6 +108,17 @@ public enum Constants {
|
||||||
DefaultTemplates.TryGetValue(templateName, out string? s) ? s : null)
|
DefaultTemplates.TryGetValue(templateName, out string? s) ? s : null)
|
||||||
?? throw new ApplicationException("Failed to retrieve mail template " + templateName + ".");
|
?? throw new ApplicationException("Failed to retrieve mail template " + templateName + ".");
|
||||||
}
|
}
|
||||||
|
public async ValueTask<string> GetPartialAsync(string partialName) {
|
||||||
|
return await FileSystem.GetPartialTemplateAsync(partialName,
|
||||||
|
DefaultTemplates.TryGetValue(partialName, out string? s) ? s : null)
|
||||||
|
?? throw new ApplicationException("Failed to retrieve mail template " + partialName + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<string> GetTemplateAsync(string templateName) {
|
||||||
|
return await FileSystem.GetEmailTemplateAsync(templateName,
|
||||||
|
DefaultTemplates.TryGetValue(templateName, out string? s) ? s : null)
|
||||||
|
?? throw new ApplicationException("Failed to retrieve mail template " + templateName + ".");
|
||||||
|
}
|
||||||
|
|
||||||
public string CompileTemplate(string template, string templateName = "unknown") {
|
public string CompileTemplate(string template, string templateName = "unknown") {
|
||||||
var options = new MjmlOptions { Beautify = false };
|
var options = new MjmlOptions { Beautify = false };
|
||||||
|
@ -116,6 +138,12 @@ public enum Constants {
|
||||||
data.TryGetValue(Enum.Parse<Constants>(token, true), out object? v) ? v?.ToString() : null);
|
data.TryGetValue(Enum.Parse<Constants>(token, true), out object? v) ? v?.ToString() : null);
|
||||||
return CompileTemplate(template, templateName);
|
return CompileTemplate(template, templateName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask<string> ProcessAsync(string templateName, Dictionary<Constants, object?> data) {
|
||||||
|
string template = ApplyTokens(await GetTemplateAsync(templateName), token =>
|
||||||
|
data.TryGetValue(Enum.Parse<Constants>(token, true), out object? v) ? v?.ToString() : null);
|
||||||
|
return CompileTemplate(template, templateName);
|
||||||
|
}
|
||||||
|
|
||||||
[GeneratedRegex(@"(\[\[.*?\]\])",
|
[GeneratedRegex(@"(\[\[.*?\]\])",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.CultureInvariant)]
|
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.CultureInvariant)]
|
||||||
|
@ -266,4 +294,19 @@ public enum Constants {
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private Dictionary<string, string> DefaultPartials { get; } = new()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"email-article",
|
||||||
|
"""
|
||||||
|
<div style="padding: 10px; background: #9f9f9f; color: #fff; margin-bottom: 10px; border-radius: 2px">
|
||||||
|
<h3 style="margin-top: 0;">{0}</h3>
|
||||||
|
<small>{1}</small>
|
||||||
|
<p>{2}...</p>
|
||||||
|
<a href="{3}">Link</a>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
|
@ -7,7 +7,7 @@ public class FileSystemService(ILogger<FileSystemService> logger) {
|
||||||
|
|
||||||
private ILogger<FileSystemService> Logger { get; } = logger;
|
private ILogger<FileSystemService> Logger { get; } = logger;
|
||||||
|
|
||||||
public Task<string?> GetEmailTemplateAsync(string name, string? defaultTemplate = null) {
|
public ValueTask<string?> GetEmailTemplateAsync(string name, string? defaultTemplate = null) {
|
||||||
string path = Path.Combine(ConfigurationDirectory, "templates", "email", name + ".mjml");
|
string path = Path.Combine(ConfigurationDirectory, "templates", "email", name + ".mjml");
|
||||||
return GetFileContentAsync(path, defaultTemplate);
|
return GetFileContentAsync(path, defaultTemplate);
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ public class FileSystemService(ILogger<FileSystemService> logger) {
|
||||||
return GetFileContent(path, defaultTemplate);
|
return GetFileContent(path, defaultTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<string?> GetPartialTemplateAsync(string name, string? defaultTemplate = null) {
|
public ValueTask<string?> GetPartialTemplateAsync(string name, string? defaultTemplate = null) {
|
||||||
string path = Path.Combine(ConfigurationDirectory, "templates", "partials", name + ".html");
|
string path = Path.Combine(ConfigurationDirectory, "templates", "partials", name + ".html");
|
||||||
return GetFileContentAsync(path, defaultTemplate);
|
return GetFileContentAsync(path, defaultTemplate);
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ public class FileSystemService(ILogger<FileSystemService> logger) {
|
||||||
return defaultContent;
|
return defaultContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private async Task<string?> GetFileContentAsync(string path, string? defaultContent = null) {
|
private async ValueTask<string?> GetFileContentAsync(string path, string? defaultContent = null) {
|
||||||
if (!File.Exists(path)) {
|
if (!File.Exists(path)) {
|
||||||
try {
|
try {
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
namespace Wave.Services;
|
namespace Wave.Services;
|
||||||
|
|
||||||
|
[Obsolete]
|
||||||
public interface IAdvancedEmailSender : IEmailSender {
|
public interface IAdvancedEmailSender : IEmailSender {
|
||||||
Task SendEmailAsync(string email, string? name, string subject, string htmlMessage);
|
Task SendEmailAsync(string email, string? name, string subject, string htmlMessage);
|
||||||
Task SendDefaultMailAsync(string receiverMail, string? receiverName, string subject, string title, string bodyHtml);
|
Task SendDefaultMailAsync(string receiverMail, string? receiverName, string subject, string title, string bodyHtml);
|
||||||
|
|
12
Wave/Services/IEmail.cs
Normal file
12
Wave/Services/IEmail.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
|
||||||
|
namespace Wave.Services;
|
||||||
|
|
||||||
|
public interface IEmail {
|
||||||
|
string ReceiverEmail { get; }
|
||||||
|
string? ReceiverName { get; }
|
||||||
|
string Subject { get; }
|
||||||
|
string Title { get; }
|
||||||
|
string ContentHtml { get; }
|
||||||
|
FrozenDictionary<string, string> Headers { get; }
|
||||||
|
}
|
8
Wave/Services/IEmailService.cs
Normal file
8
Wave/Services/IEmailService.cs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
namespace Wave.Services;
|
||||||
|
|
||||||
|
public interface IEmailService : IAsyncDisposable {
|
||||||
|
ValueTask Connect(CancellationToken cancellation);
|
||||||
|
ValueTask Disconnect(CancellationToken cancellation);
|
||||||
|
|
||||||
|
ValueTask SendEmailAsync(IEmail email);
|
||||||
|
}
|
74
Wave/Services/LiveEmailService.cs
Normal file
74
Wave/Services/LiveEmailService.cs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
using MailKit.Net.Smtp;
|
||||||
|
using MailKit.Security;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using MimeKit;
|
||||||
|
using Wave.Data;
|
||||||
|
|
||||||
|
namespace Wave.Services;
|
||||||
|
|
||||||
|
public class LiveEmailService(ILogger<LiveEmailService> logger, IOptions<SmtpConfiguration> configuration) : IEmailService {
|
||||||
|
private ILogger<LiveEmailService> Logger { get; } = logger;
|
||||||
|
private SmtpConfiguration Configuration { get; } = configuration.Value;
|
||||||
|
|
||||||
|
private SmtpClient? Client { get; set; }
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync() {
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
await Disconnect(CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask Connect(CancellationToken cancellation) {
|
||||||
|
if (Client is not null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Client = new SmtpClient();
|
||||||
|
await Client.ConnectAsync(Configuration.Host, Configuration.Port,
|
||||||
|
Configuration.Ssl ? SecureSocketOptions.SslOnConnect : SecureSocketOptions.None, cancellation);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(Configuration.Username)) {
|
||||||
|
await Client.AuthenticateAsync(Configuration.Username, Configuration.Password, cancellation);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.LogError(ex, "Error connecting to SMTP Client.");
|
||||||
|
Client?.Dispose();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask Disconnect(CancellationToken cancellation) {
|
||||||
|
if (Client is null) return;
|
||||||
|
await Client.DisconnectAsync(true, cancellation);
|
||||||
|
Client.Dispose();
|
||||||
|
Client = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask SendEmailAsync(IEmail email) {
|
||||||
|
try {
|
||||||
|
if (Client is null) throw new ApplicationException("Not connected.");
|
||||||
|
|
||||||
|
var message = new MimeMessage {
|
||||||
|
From = { new MailboxAddress(Configuration.SenderName, Configuration.SenderEmail) },
|
||||||
|
To = { new MailboxAddress(email.ReceiverName, email.ReceiverEmail) },
|
||||||
|
Subject = email.Subject
|
||||||
|
};
|
||||||
|
var builder = new BodyBuilder { HtmlBody = email.ContentHtml };
|
||||||
|
message.Body = builder.ToMessageBody();
|
||||||
|
foreach ((string id, string value) in email.Headers) {
|
||||||
|
if (id == HeaderId.ListUnsubscribe.ToHeaderName()) {
|
||||||
|
message.Headers.Add(HeaderId.ListId, $"<mailto:{Configuration.ServiceEmail ?? Configuration.SenderEmail}>");
|
||||||
|
}
|
||||||
|
message.Headers.Add(id, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Client.SendAsync(message);
|
||||||
|
Logger.LogInformation("Successfully send mail to {email} (subject: {subject}).",
|
||||||
|
email.ReceiverEmail, email.Subject);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new EmailNotSendException("Failed Email send.", ex);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.LogError(ex, "Error sending E-Mail");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,138 +1,65 @@
|
||||||
using System.Net;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using System.Text;
|
|
||||||
using MailKit.Net.Smtp;
|
|
||||||
using MailKit.Security;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using MimeKit;
|
|
||||||
using Wave.Data;
|
using Wave.Data;
|
||||||
using Wave.Utilities;
|
|
||||||
using Uri = System.Uri;
|
|
||||||
|
|
||||||
namespace Wave.Services;
|
namespace Wave.Services;
|
||||||
|
|
||||||
public class SmtpEmailSender(ILogger<SmtpEmailSender> logger, IOptions<SmtpConfiguration> config, IOptions<Customization> customizations, EmailTemplateService templateService, FileSystemService fileSystemService) : IEmailSender<ApplicationUser>, IAdvancedEmailSender {
|
public class SmtpEmailSender(EmailFactory email, [FromKeyedServices("live")]IEmailService emailService) : IEmailSender<ApplicationUser>, IAdvancedEmailSender, IAsyncDisposable {
|
||||||
private ILogger<SmtpEmailSender> Logger { get; } = logger;
|
private EmailFactory Email { get; } = email;
|
||||||
private SmtpConfiguration Configuration { get; } = config.Value;
|
private IEmailService EmailService { get; } = emailService;
|
||||||
private Customization Customizations { get; } = customizations.Value;
|
|
||||||
private FileSystemService FileSystemService { get; } = fileSystemService;
|
|
||||||
private EmailTemplateService TemplateService { get; } = templateService;
|
|
||||||
|
|
||||||
|
#region IEmailSenderAsync<ApplicationUser>
|
||||||
|
|
||||||
public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) =>
|
public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) =>
|
||||||
SendEmailAsync(email, "Confirm your email",
|
SendEmailAsync(email, user.FullName, "Confirm your email",
|
||||||
$"Please confirm your account by <a href='{confirmationLink}'>clicking here</a>.");
|
$"Please confirm your account by <a href='{confirmationLink}'>clicking here</a>.");
|
||||||
|
|
||||||
public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) =>
|
public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) =>
|
||||||
SendEmailAsync(email, "Reset your password",
|
SendEmailAsync(email, user.FullName, "Reset your password",
|
||||||
$"Please reset your password by <a href='{resetLink}'>clicking here</a>.");
|
$"Please reset your password by <a href='{resetLink}'>clicking here</a>.");
|
||||||
|
|
||||||
public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) =>
|
public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) =>
|
||||||
SendEmailAsync(email, "Reset your password",
|
SendEmailAsync(email, user.FullName, "Reset your password",
|
||||||
$"Please reset your password using the following code: {resetCode}");
|
$"Please reset your password using the following code: {resetCode}");
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region IEmailSender
|
||||||
|
|
||||||
public Task SendEmailAsync(string email, string subject, string htmlMessage) {
|
public Task SendEmailAsync(string email, string subject, string htmlMessage) {
|
||||||
return SendEmailAsync(email, null, subject, htmlMessage);
|
return SendEmailAsync(email, null, subject, htmlMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SendEmailAsync(string email, string? name, string subject, string htmlMessage)
|
#endregion
|
||||||
=> SendEmailAsync(email, name, subject, htmlMessage, []);
|
|
||||||
public async Task SendEmailAsync(string email, string? name, string subject, string htmlMessage, params Header[] header) {
|
|
||||||
try {
|
|
||||||
var message = new MimeMessage {
|
|
||||||
From = {new MailboxAddress(Configuration.SenderName, Configuration.SenderEmail)},
|
|
||||||
To = { new MailboxAddress(name, email) },
|
|
||||||
Subject = subject
|
|
||||||
};
|
|
||||||
|
|
||||||
var builder = new BodyBuilder {
|
|
||||||
HtmlBody = htmlMessage
|
|
||||||
};
|
|
||||||
|
|
||||||
message.Body = builder.ToMessageBody();
|
public async Task SendEmailAsync(string email, string? name, string subject, string htmlMessage) {
|
||||||
foreach (var h in header) {
|
await EmailService.Connect(CancellationToken.None);
|
||||||
message.Headers.Add(h);
|
await EmailService.SendEmailAsync(await Email.CreateDefaultEmail(email, name, subject, subject, htmlMessage));
|
||||||
}
|
await EmailService.Disconnect(CancellationToken.None);
|
||||||
|
|
||||||
using var client = new SmtpClient();
|
|
||||||
await client.ConnectAsync(Configuration.Host, Configuration.Port,
|
|
||||||
Configuration.Ssl ? SecureSocketOptions.SslOnConnect : SecureSocketOptions.None);
|
|
||||||
if (!string.IsNullOrWhiteSpace(Configuration.Username)) {
|
|
||||||
await client.AuthenticateAsync(Configuration.Username, Configuration.Password);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await client.SendAsync(message);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
throw new EmailNotSendException("Failed Email send.", ex);
|
|
||||||
}
|
|
||||||
await client.DisconnectAsync(true);
|
|
||||||
Logger.LogInformation("Successfully send mail to {email} (subject: {subject}).", email, subject);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.LogError(ex, "Error sending E-Mail");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SendDefaultMailAsync(string receiverMail, string? receiverName, string subject, string title, string bodyHtml) {
|
public async Task SendDefaultMailAsync(string receiverMail, string? receiverName, string subject, string title, string bodyHtml) {
|
||||||
var host = new Uri(string.IsNullOrWhiteSpace(Customizations.AppUrl) ? "" : Customizations.AppUrl); // TODO get link
|
await EmailService.Connect(CancellationToken.None);
|
||||||
string logo = !string.IsNullOrWhiteSpace(Customizations.LogoLink)
|
var email = await Email.CreateDefaultEmail(receiverMail, receiverName, subject, title, bodyHtml);
|
||||||
? Customizations.LogoLink
|
await EmailService.SendEmailAsync(email);
|
||||||
: new Uri(host, "/img/logo.png").AbsoluteUri;
|
await EmailService.Disconnect(CancellationToken.None);
|
||||||
string body = TemplateService.Default(host.AbsoluteUri, logo, title, bodyHtml);
|
|
||||||
return SendEmailAsync(receiverMail, receiverName, subject, body);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendSubscribedMailAsync(EmailSubscriber subscriber, string subject, string title, string bodyHtml,
|
public async Task SendSubscribedMailAsync(EmailSubscriber subscriber, string subject, string title, string bodyHtml,
|
||||||
string browserUrl = "", string subscribedRole = "-1") {
|
string browserUrl = "", string subscribedRole = "-1") {
|
||||||
(string user, string token) = await TemplateService
|
var email = await Email.CreateSubscribedEmail(subscriber, browserUrl, subject, title, bodyHtml, subscribedRole);
|
||||||
.CreateConfirmTokensAsync(subscriber.Id, "unsubscribe-"+subscribedRole, TimeSpan.FromDays(30));
|
await EmailService.Connect(CancellationToken.None);
|
||||||
var host = new Uri(string.IsNullOrWhiteSpace(Customizations.AppUrl) ? "" : Customizations.AppUrl); // TODO get link
|
await EmailService.SendEmailAsync(email); // TODO use bulk service
|
||||||
browserUrl = string.IsNullOrWhiteSpace(browserUrl) ? host.AbsoluteUri : browserUrl; // TODO find better solution
|
|
||||||
|
|
||||||
string logo = !string.IsNullOrWhiteSpace(Customizations.LogoLink)
|
|
||||||
? Customizations.LogoLink
|
|
||||||
: new Uri(host, "/img/logo.png").AbsoluteUri;
|
|
||||||
string unsubscribeLink = new Uri(host,
|
|
||||||
$"/Email/Unsubscribe?newsletter={subscribedRole}&user={WebUtility.UrlEncode(user)}&token={WebUtility.UrlEncode(token)}").AbsoluteUri;
|
|
||||||
string body = TemplateService.Newsletter(host.AbsoluteUri, browserUrl, logo, title, bodyHtml, unsubscribeLink);
|
|
||||||
await SendEmailAsync(subscriber.Email, subscriber.Name, subject, body,
|
|
||||||
new Header(HeaderId.ListUnsubscribe, $"<{unsubscribeLink}>"),
|
|
||||||
new Header(HeaderId.ListUnsubscribePost, "One-Click"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendWelcomeMailAsync(EmailSubscriber subscriber, string subject, string title, string bodyHtml,
|
public async Task SendWelcomeMailAsync(EmailSubscriber subscriber, string subject, string title, string bodyHtml,
|
||||||
IEnumerable<EmailNewsletter> articles) {
|
IEnumerable<EmailNewsletter> articles) {
|
||||||
(string user, string token) = await TemplateService
|
var email = await Email.CreateWelcomeEmail(subscriber, articles, subject, title, bodyHtml);
|
||||||
.CreateConfirmTokensAsync(subscriber.Id, "unsubscribe-welcome", TimeSpan.FromDays(30));
|
await EmailService.Connect(CancellationToken.None);
|
||||||
var host = new Uri(string.IsNullOrWhiteSpace(Customizations.AppUrl) ? "" : Customizations.AppUrl); // TODO get link
|
await EmailService.SendEmailAsync(email); // TODO use bulk service
|
||||||
|
|
||||||
string articlePartial = (await FileSystemService.GetPartialTemplateAsync("email-article", """
|
|
||||||
<div style="padding: 10px; background: #9f9f9f; color: #fff; margin-bottom: 10px; border-radius: 2px">
|
|
||||||
<h3 style="margin-top: 0;">{0}</h3>
|
|
||||||
<small>{1}</small>
|
|
||||||
<p>{2}...</p>
|
|
||||||
<a href="{3}">Link</a>
|
|
||||||
</div>
|
|
||||||
"""))!;
|
|
||||||
var articlesHtml = new StringBuilder("");
|
|
||||||
foreach (var n in articles) {
|
|
||||||
string articleLink = ArticleUtilities.GenerateArticleLink(n.Article, new Uri(Customizations.AppUrl, UriKind.Absolute));
|
|
||||||
articlesHtml.AppendFormat(
|
|
||||||
articlePartial,
|
|
||||||
n.Article.Title, n.Article.Author.Name, n.Article.Body[..Math.Min(250, n.Article.Body.Length)], articleLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
string logo = !string.IsNullOrWhiteSpace(Customizations.LogoLink)
|
|
||||||
? Customizations.LogoLink
|
|
||||||
: new Uri(host, "/img/logo.png").AbsoluteUri;
|
|
||||||
string unsubscribeLink = new Uri(host,
|
|
||||||
$"/Email/Unsubscribe?newsletter=welcome&user={WebUtility.UrlEncode(user)}&token={WebUtility.UrlEncode(token)}").AbsoluteUri;
|
|
||||||
string body = TemplateService.Welcome(host.AbsoluteUri, logo, title, bodyHtml, unsubscribeLink, articlesHtml.ToString());
|
|
||||||
await SendEmailAsync(subscriber.Email, subscriber.Name, subject, body,
|
|
||||||
new Header(HeaderId.ListUnsubscribe, $"<{unsubscribeLink}>"),
|
|
||||||
new Header(HeaderId.ListUnsubscribePost, "One-Click"));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public class EmailNotSendException(string message, Exception exception) : ApplicationException(message, exception);
|
public async ValueTask DisposeAsync() {
|
||||||
|
await EmailService.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
8
Wave/Services/StaticEmail.cs
Normal file
8
Wave/Services/StaticEmail.cs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Mjml.Net.Helpers;
|
||||||
|
|
||||||
|
namespace Wave.Services;
|
||||||
|
|
||||||
|
public record StaticEmail(string ReceiverEmail, string? ReceiverName, string Subject, string Title, string ContentHtml,
|
||||||
|
FrozenDictionary<string, string> Headers) : IEmail;
|
Loading…
Reference in a new issue