add postmark as default mail dispatcher

This commit is contained in:
2026-03-05 15:06:28 +01:00
parent 290d246a4f
commit 42b5732808
6 changed files with 212 additions and 2 deletions
+25
View File
@@ -1,4 +1,5 @@
using System.Net.Mail;
using System.Text;
namespace zero.Mails;
@@ -26,6 +27,30 @@ public class Mail : MailMessage
public MailPlaceholders Placeholders { get; set; } = new();
public MailMetadata Metadata { get; set; } = new();
/// <summary>
/// Set To and From addresses based on values in MailOptions
/// </summary>
public Mail For(MailOptions options)
{
To.Add(new MailAddress(options.To, options.ToName));
From = new MailAddress(options.From, options.FromName);
return this;
}
/// <summary>
/// Sets the body as plain text
/// </summary>
public string PlainText
{
get => Body;
set
{
Body = value;
IsBodyHtml = false;
BodyEncoding = Encoding.UTF8;
}
}
}
public class MailMetadata : Dictionary<string, string>
+12 -1
View File
@@ -10,12 +10,23 @@ public class MailOptions
//public string Password { get; set; }
//public string To { get; set; }
public string From { get; set; }
public string FromName { get; set; }
public string To { get; set; }
public string ToName { get; set; }
public string ReplyTo { get; set; }
public bool Debug { get; set; } = true;
public string SenderEmail { get; set; }
public string SenderName { get; set; }
public PostmarkOptions Postmark { get; set; } = new();
public Func<Mail, string> BuildViewPath { get; set; }
}
+162
View File
@@ -0,0 +1,162 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using PostmarkDotNet;
using PostmarkDotNet.Model;
namespace zero.Mails;
public class PostmarkDispatcher : IMailDispatcher
{
protected Queue<Mail> Queue { get; private set; } = new();
protected PostmarkClient Postmark { get; set; }
protected PostmarkAdminClient PostmarkAdmin { get; set; }
protected MailOptions Options { get; set; }
protected IWebHostEnvironment Env { get; set; }
public PostmarkDispatcher(IOptionsMonitor<MailOptions> monitor, IWebHostEnvironment env)
{
Options = monitor.CurrentValue;
Postmark = new(Options.Postmark.ServerToken);
PostmarkAdmin = new(Options.Postmark.AccountToken);
Env = env;
monitor.OnChange(opts =>
{
Options = opts;
Postmark = new(Options.Postmark.ServerToken);
});
}
/// <inheritdoc />
public async Task<bool> IsSenderSupported(string email)
{
if (email.IsNullOrWhiteSpace() || Options.Postmark.AccountToken.IsNullOrEmpty())
{
return true;
}
try
{
email = email.FullTrim();
PostmarkSenderSignatureList signatures = await PostmarkAdmin.GetSenderSignaturesAsync();
return signatures.SenderSignatures.Any(x => x.Confirmed && x.EmailAddress.Equals(email, StringComparison.InvariantCultureIgnoreCase));
}
catch
{
return true;
}
}
/// <inheritdoc />
public void Enqueue(Mail message)
{
Queue.Enqueue(message);
}
/// <inheritdoc />
public async Task Send(CancellationToken token = default)
{
HashSet<PostmarkMessage> messages = [];
while (Queue.Count > 0)
{
Mail message = Queue.Dequeue();
PostmarkMessage data = new()
{
// to addresses
To = message.To.ToString(),
Cc = message.CC.ToString(),
Bcc = message.Bcc.ToString(),
// from address
From = message.From.ToString(),
ReplyTo = message.ReplyToList.ToString(),
// subject
Subject = message.Subject,
// tracking
TrackLinks = LinkTrackingOptions.None,
TrackOpens = false,
// configuration
MessageStream = Options.Postmark.MessageStream,
//Tag = message.Template?.Key,
Metadata = message.Metadata
};
// set attachments
foreach (System.Net.Mail.Attachment attachment in message.Attachments)
{
data.AddAttachment(attachment.ContentStream, attachment.Name, attachment.ContentType.MediaType, attachment.ContentId);
}
// set body
if (!message.IsBodyHtml)
{
data.TextBody = message.Body;
}
else
{
data.HtmlBody = message.Body;
}
// overwrite for debug mode
if (Options.Debug || (Env != null && Env.IsDevelopment()))
{
data.From = "noreply@post.swcs.pro";
string[] allowedTlds = ["swcs.pro", "alias.swcs.pro"];
string tld = data.To.TrimEnd('>').Split('@').LastOrDefault();
data.Cc = null; // "cee-maildev@gmx.at,anaheimcore@gmail.com,ceemaildev@yahoo.com";
data.Bcc = null;
if (!allowedTlds.Contains(tld, StringComparer.InvariantCultureIgnoreCase))
{
data.Subject = $"{data.Subject} (test; für {data.To})";
data.To = "maildev@alias.swcs.pro";
}
else
{
data.Subject = $"{data.Subject} (test)";
}
}
messages.Add(data);
}
// finally sends the message
IEnumerable<PostmarkResponse> responses = await Postmark.SendMessagesAsync(messages);
foreach (PostmarkResponse response in responses)
{
if (response.ErrorCode > 0)
{
throw new PostmarkSendException($"Could not send message via Postmark API. Code: {response.ErrorCode}, Message: {response.Message}");
}
}
}
/// <inheritdoc />
public void Dispose() { }
}
public class PostmarkSendException : Exception
{
public PostmarkSendException() : base() { }
public PostmarkSendException(string message) : base(message) { }
}
+10
View File
@@ -0,0 +1,10 @@
namespace zero.Mails;
public class PostmarkOptions
{
public string ServerToken { get; set; }
public string AccountToken { get; set; }
public string MessageStream { get; set; } = "outbound";
}
+2 -1
View File
@@ -8,7 +8,8 @@ internal class ZeroMailModule : ZeroModule
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddScoped<IMailProvider, MailProvider>();
services.AddScoped<IMailDispatcher, LoggerMailDispatcher>();
//services.AddScoped<IMailDispatcher, LoggerMailDispatcher>();
services.AddScoped<IMailDispatcher, PostmarkDispatcher>();
services.AddOptions<MailOptions>().Bind(configuration.GetSection("Zero:Mails")).Configure(opts =>
{
+1
View File
@@ -16,6 +16,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="9.0.4" />
<PackageReference Include="FluentValidation" Version="11.11.0" />
<PackageReference Include="Postmark" Version="5.3.0" />
<PackageReference Include="PowCapServer.AspNetCore" Version="2.0.0" />
<PackageReference Include="SixLabors.ImageSharp.Web" Version="3.2.0" />
</ItemGroup>