mail module

This commit is contained in:
2023-07-18 10:59:06 +02:00
parent 7d28f23881
commit b890be0f6d
9 changed files with 343 additions and 1 deletions
@@ -1,4 +1,5 @@
using Raven.Client.Documents.Linq;
using System.Linq.Expressions;
namespace zero.Raven;
@@ -15,6 +16,10 @@ public static class RavenOperationsExtensions
/// </summary>
public static async Task<Result<T>> Delete<T>(this IRavenOperations ops, string id) where T : ZeroIdEntity, new() => await ops.Delete(await ops.Load<T>(id));
/// <summary>
/// Deletes entities by selector
/// </summary>
public static async Task<int> Delete<T>(this IRavenOperations ops, Expression<Func<T, bool>> predicate) where T : ZeroIdEntity, new() => await ops.Delete(await ops.Load<T>(predicate));
/// <summary>
/// Deletes entities by Id
+88
View File
@@ -0,0 +1,88 @@
//using System.IO;
//using System.Text;
//namespace zero.Mails;
///// <summary>
///// Default implementation of an IMailSender which sends the mail a flat file
///// and therefore not using the SMTP channel.
///// Implementing real mail sending is up to the consuming application.
///// </summary>
//public class FileMailDispatcher : IMailDispatcher
//{
// protected Queue<Mail> Queue { get; private set; } = new Queue<Mail>();
// protected IPaths PathResolver { get; private set; }
// string MailDirectory;
// public FileMailDispatcher(IPaths pathResolver)
// {
// PathResolver = pathResolver;
// MailDirectory = "mails";
// }
// /// <inheritdoc />
// public void Enqueue(Mail message)
// {
// Queue.Enqueue(message);
// }
// /// <inheritdoc />
// public async Task Send(CancellationToken token = default)
// {
// if (Queue.Count < 1)
// {
// return;
// }
// string folder = PathResolver.Map(MailDirectory);
// PathResolver.Create(folder);
// while (Queue.Count > 0)
// {
// Mail message = Queue.Dequeue();
// string content = JsonConvert.SerializeObject(message, new JsonSerializerSettings()
// {
// Formatting = Formatting.Indented,
// TypeNameHandling = TypeNameHandling.None,
// ReferenceLoopHandling = ReferenceLoopHandling.Ignore
// });
// string filename = Safenames.File(DateTimeOffset.Now.ToString("yyyy-MM-dd-HH-mm-ss") + "_" + message.To[0].Address + ".txt");
// string path = PathResolver.Map(folder, filename);
// await File.WriteAllTextAsync(path, content, token);
// }
// }
// /// <inheritdoc />
// public void Dispose() { }
// /// <summary>
// /// Creats the file content from a mail message
// /// </summary>
// string CreateContent(Mail message)
// {
// StringBuilder text = new();
// text.AppendLine("To: " + message.To);
// text.AppendLine("CC: " + message.CC);
// text.AppendLine("Bcc: " + message.Bcc);
// text.AppendLine("From: " + message.From);
// text.AppendLine("Subject: " + message.Subject);
// text.Append("Date: " + DateTimeOffset.UtcNow.ToString());
// text.AppendLine();
// text.AppendLine("=============== BODY ===============");
// text.AppendLine();
// text.AppendLine(message.Body);
// return text.ToString();
// }
//}
+19
View File
@@ -0,0 +1,19 @@
namespace zero.Mails;
public interface IMailDispatcher : IDisposable
{
/// <summary>
/// Adds a new mail message to the outgoing queue
/// </summary>
void Enqueue(Mail message);
/// <summary>
/// Sends all mails which have been added to the queue previously
/// </summary>
Task Send(CancellationToken token = default);
/// <summary>
/// Whether a certain sender signature is supported by this dispatcher
/// </summary>
Task<bool> IsSenderSupported(string email) => Task.FromResult(true);
}
+45
View File
@@ -0,0 +1,45 @@
using Microsoft.Extensions.Logging;
namespace zero.Mails;
/// <summary>
/// Default implementation of an IMailSender which sends the mail to the attached logger
/// and therefore not using the SMTP channel.
/// Implementing real mail sending is up to the consuming application.
/// </summary>
public class LoggerMailDispatcher : IMailDispatcher
{
protected Queue<Mail> Queue { get; private set; } = new Queue<Mail>();
protected ILogger<LoggerMailDispatcher> Logger { get; set; }
public LoggerMailDispatcher(ILogger<LoggerMailDispatcher> logger)
{
Logger = logger;
}
/// <inheritdoc />
public void Enqueue(Mail message)
{
Queue.Enqueue(message);
}
/// <inheritdoc />
public Task Send(CancellationToken token = default)
{
while (Queue.Count > 0)
{
Mail message = Queue.Dequeue();
Logger.LogInformation("Mail to {to}. Subject: {subject}", message.To[0].Address, message.Subject);
}
return Task.CompletedTask;
}
/// <inheritdoc />
public void Dispose() { }
}
+39
View File
@@ -0,0 +1,39 @@
using System.Net.Mail;
namespace zero.Mails;
public class Mail<T> : Mail where T : class
{
public T Model { get; set; }
}
public class Mail : MailMessage
{
public bool IsDeactivated { get; set; }
public string ViewKey { get; set; }
public string ViewPath { get; set; }
public string Tag { get; set; }
public bool HasView { get; set; } = true;
public bool IsRendered { get; set; }
public string Preheader { get; set; }
public MailPlaceholders Placeholders { get; set; } = new();
public MailMetadata Metadata { get; set; } = new();
}
public class MailMetadata : Dictionary<string, string>
{
}
public class MailPlaceholders : Dictionary<string, string>
{
}
+10
View File
@@ -0,0 +1,10 @@
namespace zero.Mails;
public class MailOptions
{
public string SenderEmail { get; set; }
public string SenderName { get; set; }
public Func<Mail, string> BuildViewPath { get; set; }
}
+117
View File
@@ -0,0 +1,117 @@
using Microsoft.Extensions.Logging;
using System.Net.Mail;
using System.Text;
namespace zero.Mails;
public class MailProvider : IMailProvider
{
protected ILogger<IMailProvider> Logger { get; set; }
protected IZeroContext Zero { get; set; }
protected IMailDispatcher MailSender { get; set; }
protected IRazorRenderer Renderer { get; set; }
protected MailOptions Options { get; set; }
private Encoding encoding = Encoding.UTF8;
public MailProvider(IZeroContext zero, ILogger<IMailProvider> logger, IMailDispatcher mailSender, IRazorRenderer renderer)
{
Zero = zero;
Logger = logger;
MailSender = mailSender;
Renderer = renderer;
Options = zero.Options.For<MailOptions>();
}
/// <inheritdoc />
public virtual async Task Send(Mail message, CancellationToken token = default)
{
await Send(message, MailSender, token);
}
/// <inheritdoc />
public virtual async Task Send(Mail message, IMailDispatcher dispatcher, CancellationToken token = default)
{
if (message.IsDeactivated)
{
return;
}
try
{
await Prepare(message);
dispatcher.Enqueue(message);
await dispatcher.Send(token);
Logger.LogInformation("Dispatched email to {recipient} (cc: {cc}, bcc: {bcc})", message.To, message.CC, message.Bcc);
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to send mail message");
}
}
/// <inheritdoc />
public virtual async Task<string> Prepare(Mail message)
{
message.From ??= new MailAddress(Options.SenderEmail, Options.SenderName, encoding);
message.Sender ??= message.From;
if (message.ReplyToList.Count < 1)
{
message.ReplyToList.Add(message.From);
}
message.Subject = TokenReplacement.Apply(message.Subject, message.Placeholders);
message.SubjectEncoding = encoding;
message.Body = TokenReplacement.Apply(message.Body, message.Placeholders);
message.BodyEncoding = encoding;
message.Preheader = TokenReplacement.Apply(message.Preheader, message.Placeholders);
if (!message.HasView || message.Body.HasValue())
{
message.IsRendered = true;
return message.Body;
}
string viewPath = message.ViewPath;
if (viewPath.IsNullOrEmpty())
{
viewPath = Options.BuildViewPath(message);
}
message.Body = await Renderer.ViewAsync(viewPath, message);
message.IsBodyHtml = true;
message.IsRendered = true;
return message.Body;
}
}
public interface IMailProvider
{
/// <summary>
/// Renders the message body.
/// This is automatically called when sending messages.
/// </summary>
Task<string> Prepare(Mail message);
/// <summary>
/// Sends a message with the default dispatcher
/// </summary>
Task Send(Mail message, CancellationToken token = default);
/// <summary>
/// Sends a message with the specified dispatcher
/// </summary>
Task Send(Mail message, IMailDispatcher dispatcher, CancellationToken token = default);
}
+18
View File
@@ -0,0 +1,18 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace zero.Mails;
internal class ZeroMailModule : ZeroModule
{
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddScoped<IMailProvider, MailProvider>();
services.AddScoped<IMailDispatcher, LoggerMailDispatcher>();
services.AddOptions<MailOptions>().Bind(configuration.GetSection("Zero:Mails")).Configure(opts =>
{
opts.BuildViewPath = mail => $"~/Mails/{mail.ViewKey.Replace('.', '/')}.cshtml";
});
}
}
+2 -1
View File
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using zero.Mails;
using zero.Mvc;
using zero.Numbers;
using zero.Routing;
@@ -52,7 +53,7 @@ public class ZeroBuilder
Modules.Add<ZeroFileStorageModule>();
//Modules.Add<ZeroIdentityModule>();
Modules.Add<ZeroLocalizationModule>();
//Modules.Add<ZeroMailModule>();
Modules.Add<ZeroMailModule>();
//Modules.Add<ZeroMapperModule>();
Modules.Add<ZeroMediaModule>();
//Modules.Add<ZeroPageModule>();