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 SenderEmail { get; init; }
|
||||
public required string SenderName { get; init; }
|
||||
public required string ServiceEmail { get; init; }
|
||||
public bool Ssl { get; init; } = true;
|
||||
}
|
|
@ -124,6 +124,7 @@
|
|||
var smtpConfig = builder.Configuration.GetSection("Email:Smtp");
|
||||
if (smtpConfig.Exists()) {
|
||||
builder.Services.Configure<SmtpConfiguration>(smtpConfig);
|
||||
builder.Services.AddKeyedScoped<IEmailService, LiveEmailService>("live");
|
||||
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
|
||||
builder.Services.AddScoped<IAdvancedEmailSender, SmtpEmailSender>();
|
||||
builder.Services.AddScoped<IEmailSender<ApplicationUser>, SmtpEmailSender>();
|
||||
|
@ -132,6 +133,8 @@
|
|||
logMessages.Add("No Email provider configured.");
|
||||
}
|
||||
|
||||
builder.Services.AddScoped<EmailFactory>();
|
||||
|
||||
builder.Services.AddSingleton<IMessageDisplay, MessageService>();
|
||||
builder.Services.AddSingleton<FileSystemService>();
|
||||
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) {
|
||||
return Process("newsletter", new Dictionary<Constants, object?> {
|
||||
{ Constants.HomeLink, home },
|
||||
|
@ -61,6 +70,16 @@ public enum Constants {
|
|||
{ 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) {
|
||||
return Process("welcome", new Dictionary<Constants, object?> {
|
||||
|
@ -77,15 +96,7 @@ public enum Constants {
|
|||
FileSystem.GetEmailTemplate("default", DefaultTemplates["default"]);
|
||||
FileSystem.GetEmailTemplate("newsletter", DefaultTemplates["newsletter"]);
|
||||
FileSystem.GetEmailTemplate("welcome", DefaultTemplates["welcome"]);
|
||||
FileSystem.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>
|
||||
""");
|
||||
FileSystem.GetPartialTemplate("email-article", DefaultPartials["email-article"]);
|
||||
}
|
||||
|
||||
public string ApplyTokens(string template, Func<string, string?> replacer) {
|
||||
|
@ -97,6 +108,17 @@ public enum Constants {
|
|||
DefaultTemplates.TryGetValue(templateName, out string? s) ? s : null)
|
||||
?? 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") {
|
||||
var options = new MjmlOptions { Beautify = false };
|
||||
|
@ -117,6 +139,12 @@ public enum Constants {
|
|||
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(@"(\[\[.*?\]\])",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex MyRegex();
|
||||
|
@ -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;
|
||||
|
||||
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");
|
||||
return GetFileContentAsync(path, defaultTemplate);
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ public class FileSystemService(ILogger<FileSystemService> logger) {
|
|||
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");
|
||||
return GetFileContentAsync(path, defaultTemplate);
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ public class FileSystemService(ILogger<FileSystemService> logger) {
|
|||
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)) {
|
||||
try {
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
namespace Wave.Services;
|
||||
|
||||
[Obsolete]
|
||||
public interface IAdvancedEmailSender : IEmailSender {
|
||||
Task SendEmailAsync(string email, string? name, string subject, string htmlMessage);
|
||||
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 System.Text;
|
||||
using MailKit.Net.Smtp;
|
||||
using MailKit.Security;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MimeKit;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Wave.Data;
|
||||
using Wave.Utilities;
|
||||
using Uri = System.Uri;
|
||||
|
||||
namespace Wave.Services;
|
||||
|
||||
public class SmtpEmailSender(ILogger<SmtpEmailSender> logger, IOptions<SmtpConfiguration> config, IOptions<Customization> customizations, EmailTemplateService templateService, FileSystemService fileSystemService) : IEmailSender<ApplicationUser>, IAdvancedEmailSender {
|
||||
private ILogger<SmtpEmailSender> Logger { get; } = logger;
|
||||
private SmtpConfiguration Configuration { get; } = config.Value;
|
||||
private Customization Customizations { get; } = customizations.Value;
|
||||
private FileSystemService FileSystemService { get; } = fileSystemService;
|
||||
private EmailTemplateService TemplateService { get; } = templateService;
|
||||
public class SmtpEmailSender(EmailFactory email, [FromKeyedServices("live")]IEmailService emailService) : IEmailSender<ApplicationUser>, IAdvancedEmailSender, IAsyncDisposable {
|
||||
private EmailFactory Email { get; } = email;
|
||||
private IEmailService EmailService { get; } = emailService;
|
||||
|
||||
#region IEmailSenderAsync<ApplicationUser>
|
||||
|
||||
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>.");
|
||||
|
||||
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>.");
|
||||
|
||||
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}");
|
||||
|
||||
#endregion
|
||||
|
||||
#region IEmailSender
|
||||
|
||||
public Task SendEmailAsync(string email, string subject, string htmlMessage) {
|
||||
return SendEmailAsync(email, null, subject, htmlMessage);
|
||||
}
|
||||
|
||||
public Task SendEmailAsync(string email, string? name, string subject, string htmlMessage)
|
||||
=> 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
|
||||
};
|
||||
#endregion
|
||||
|
||||
var builder = new BodyBuilder {
|
||||
HtmlBody = htmlMessage
|
||||
};
|
||||
|
||||
message.Body = builder.ToMessageBody();
|
||||
foreach (var h in header) {
|
||||
message.Headers.Add(h);
|
||||
}
|
||||
|
||||
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 async Task SendEmailAsync(string email, string? name, string subject, string htmlMessage) {
|
||||
await EmailService.Connect(CancellationToken.None);
|
||||
await EmailService.SendEmailAsync(await Email.CreateDefaultEmail(email, name, subject, subject, htmlMessage));
|
||||
await EmailService.Disconnect(CancellationToken.None);
|
||||
}
|
||||
|
||||
public 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
|
||||
string logo = !string.IsNullOrWhiteSpace(Customizations.LogoLink)
|
||||
? Customizations.LogoLink
|
||||
: new Uri(host, "/img/logo.png").AbsoluteUri;
|
||||
string body = TemplateService.Default(host.AbsoluteUri, logo, title, bodyHtml);
|
||||
return SendEmailAsync(receiverMail, receiverName, subject, body);
|
||||
public async Task SendDefaultMailAsync(string receiverMail, string? receiverName, string subject, string title, string bodyHtml) {
|
||||
await EmailService.Connect(CancellationToken.None);
|
||||
var email = await Email.CreateDefaultEmail(receiverMail, receiverName, subject, title, bodyHtml);
|
||||
await EmailService.SendEmailAsync(email);
|
||||
await EmailService.Disconnect(CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task SendSubscribedMailAsync(EmailSubscriber subscriber, string subject, string title, string bodyHtml,
|
||||
string browserUrl = "", string subscribedRole = "-1") {
|
||||
(string user, string token) = await TemplateService
|
||||
.CreateConfirmTokensAsync(subscriber.Id, "unsubscribe-"+subscribedRole, TimeSpan.FromDays(30));
|
||||
var host = new Uri(string.IsNullOrWhiteSpace(Customizations.AppUrl) ? "" : Customizations.AppUrl); // TODO get link
|
||||
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"));
|
||||
var email = await Email.CreateSubscribedEmail(subscriber, browserUrl, subject, title, bodyHtml, subscribedRole);
|
||||
await EmailService.Connect(CancellationToken.None);
|
||||
await EmailService.SendEmailAsync(email); // TODO use bulk service
|
||||
}
|
||||
|
||||
public async Task SendWelcomeMailAsync(EmailSubscriber subscriber, string subject, string title, string bodyHtml,
|
||||
IEnumerable<EmailNewsletter> articles) {
|
||||
(string user, string token) = await TemplateService
|
||||
.CreateConfirmTokensAsync(subscriber.Id, "unsubscribe-welcome", TimeSpan.FromDays(30));
|
||||
var host = new Uri(string.IsNullOrWhiteSpace(Customizations.AppUrl) ? "" : Customizations.AppUrl); // TODO get link
|
||||
var email = await Email.CreateWelcomeEmail(subscriber, articles, subject, title, bodyHtml);
|
||||
await EmailService.Connect(CancellationToken.None);
|
||||
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 async ValueTask DisposeAsync() {
|
||||
await EmailService.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class EmailNotSendException(string message, Exception exception) : ApplicationException(message, exception);
|
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