diff --git a/zero.Raven/Operations/RavenOperationsExtensions.cs b/zero.Raven/Operations/RavenOperationsExtensions.cs index 5841cb3a..01c18d0a 100644 --- a/zero.Raven/Operations/RavenOperationsExtensions.cs +++ b/zero.Raven/Operations/RavenOperationsExtensions.cs @@ -1,4 +1,5 @@ using Raven.Client.Documents.Linq; +using System.Linq.Expressions; namespace zero.Raven; @@ -15,6 +16,10 @@ public static class RavenOperationsExtensions /// public static async Task> Delete(this IRavenOperations ops, string id) where T : ZeroIdEntity, new() => await ops.Delete(await ops.Load(id)); + /// + /// Deletes entities by selector + /// + public static async Task Delete(this IRavenOperations ops, Expression> predicate) where T : ZeroIdEntity, new() => await ops.Delete(await ops.Load(predicate)); /// /// Deletes entities by Id diff --git a/zero/Mails/FileMailDispatcher.cs b/zero/Mails/FileMailDispatcher.cs new file mode 100644 index 00000000..de3187b8 --- /dev/null +++ b/zero/Mails/FileMailDispatcher.cs @@ -0,0 +1,88 @@ +//using System.IO; +//using System.Text; + +//namespace zero.Mails; + +///// +///// 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. +///// +//public class FileMailDispatcher : IMailDispatcher +//{ +// protected Queue Queue { get; private set; } = new Queue(); + +// protected IPaths PathResolver { get; private set; } + +// string MailDirectory; + + +// public FileMailDispatcher(IPaths pathResolver) +// { +// PathResolver = pathResolver; +// MailDirectory = "mails"; +// } + + +// /// +// public void Enqueue(Mail message) +// { +// Queue.Enqueue(message); +// } + + +// /// +// 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); +// } +// } + + +// /// +// public void Dispose() { } + + +// /// +// /// Creats the file content from a mail message +// /// +// 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(); +// } +//} diff --git a/zero/Mails/IMailDispatcher.cs b/zero/Mails/IMailDispatcher.cs new file mode 100644 index 00000000..4f1d95d1 --- /dev/null +++ b/zero/Mails/IMailDispatcher.cs @@ -0,0 +1,19 @@ +namespace zero.Mails; + +public interface IMailDispatcher : IDisposable +{ + /// + /// Adds a new mail message to the outgoing queue + /// + void Enqueue(Mail message); + + /// + /// Sends all mails which have been added to the queue previously + /// + Task Send(CancellationToken token = default); + + /// + /// Whether a certain sender signature is supported by this dispatcher + /// + Task IsSenderSupported(string email) => Task.FromResult(true); +} \ No newline at end of file diff --git a/zero/Mails/LoggerMailDispatcher.cs b/zero/Mails/LoggerMailDispatcher.cs new file mode 100644 index 00000000..9fb4f5e2 --- /dev/null +++ b/zero/Mails/LoggerMailDispatcher.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.Logging; + +namespace zero.Mails; + +/// +/// 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. +/// +public class LoggerMailDispatcher : IMailDispatcher +{ + protected Queue Queue { get; private set; } = new Queue(); + + protected ILogger Logger { get; set; } + + + public LoggerMailDispatcher(ILogger logger) + { + Logger = logger; + } + + + /// + public void Enqueue(Mail message) + { + Queue.Enqueue(message); + } + + + /// + 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; + } + + + /// + public void Dispose() { } +} \ No newline at end of file diff --git a/zero/Mails/Mail.cs b/zero/Mails/Mail.cs new file mode 100644 index 00000000..08515087 --- /dev/null +++ b/zero/Mails/Mail.cs @@ -0,0 +1,39 @@ +using System.Net.Mail; + +namespace zero.Mails; + +public class Mail : 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 +{ + +} + +public class MailPlaceholders : Dictionary +{ + +} \ No newline at end of file diff --git a/zero/Mails/MailOptions.cs b/zero/Mails/MailOptions.cs new file mode 100644 index 00000000..f198302b --- /dev/null +++ b/zero/Mails/MailOptions.cs @@ -0,0 +1,10 @@ +namespace zero.Mails; + +public class MailOptions +{ + public string SenderEmail { get; set; } + + public string SenderName { get; set; } + + public Func BuildViewPath { get; set; } +} \ No newline at end of file diff --git a/zero/Mails/MailProvider.cs b/zero/Mails/MailProvider.cs new file mode 100644 index 00000000..6ec7a377 --- /dev/null +++ b/zero/Mails/MailProvider.cs @@ -0,0 +1,117 @@ +using Microsoft.Extensions.Logging; +using System.Net.Mail; +using System.Text; + +namespace zero.Mails; + +public class MailProvider : IMailProvider +{ + protected ILogger 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 logger, IMailDispatcher mailSender, IRazorRenderer renderer) + { + Zero = zero; + Logger = logger; + MailSender = mailSender; + Renderer = renderer; + Options = zero.Options.For(); + } + + + /// + public virtual async Task Send(Mail message, CancellationToken token = default) + { + await Send(message, MailSender, token); + } + + + /// + 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"); + } + } + + + /// + public virtual async Task 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 +{ + /// + /// Renders the message body. + /// This is automatically called when sending messages. + /// + Task Prepare(Mail message); + + /// + /// Sends a message with the default dispatcher + /// + Task Send(Mail message, CancellationToken token = default); + + /// + /// Sends a message with the specified dispatcher + /// + Task Send(Mail message, IMailDispatcher dispatcher, CancellationToken token = default); +} diff --git a/zero/Mails/ZeroMailModule.cs b/zero/Mails/ZeroMailModule.cs new file mode 100644 index 00000000..af6a7ccd --- /dev/null +++ b/zero/Mails/ZeroMailModule.cs @@ -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(); + services.AddScoped(); + + services.AddOptions().Bind(configuration.GetSection("Zero:Mails")).Configure(opts => + { + opts.BuildViewPath = mail => $"~/Mails/{mail.ViewKey.Replace('.', '/')}.cshtml"; + }); + } +} \ No newline at end of file diff --git a/zero/ZeroBuilder.cs b/zero/ZeroBuilder.cs index 4569b257..c7447631 100644 --- a/zero/ZeroBuilder.cs +++ b/zero/ZeroBuilder.cs @@ -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(); //Modules.Add(); Modules.Add(); - //Modules.Add(); + Modules.Add(); //Modules.Add(); Modules.Add(); //Modules.Add();