From 017faf601caa817cbf56f574aa4902bd29664643 Mon Sep 17 00:00:00 2001 From: jung Date: Fri, 19 Jan 2024 00:21:44 +0100 Subject: [PATCH] replyto --- Core.Email.Abstractions/CoreEmailMessage.cs | 3 ++ Core.Email.Abstractions/CoreEmailStatus.cs | 1 + .../ICoreEmailPersistence.cs | 2 +- .../MailjetProvider.cs | 1 + .../PostmarkProvider.cs | 6 ++- .../SimpleEmailServiceProvider.cs | 5 +++ Core.Email.Provider.SMTP/SmtpProvider.cs | 3 ++ .../SendGridProvider.cs | 6 ++- Core.Email.Tests/EmailTest.cs | 15 +++++-- Core.Email/CoreEmailService.cs | 40 ++++++++++++------- README.md | 10 +++-- 11 files changed, 67 insertions(+), 25 deletions(-) diff --git a/Core.Email.Abstractions/CoreEmailMessage.cs b/Core.Email.Abstractions/CoreEmailMessage.cs index f1c4cc4..2afe846 100644 --- a/Core.Email.Abstractions/CoreEmailMessage.cs +++ b/Core.Email.Abstractions/CoreEmailMessage.cs @@ -7,8 +7,11 @@ public class CoreEmailMessage public List Cc { get; init; } = new(); public List Bcc { get; init; } = new(); public string From { get; init; } = string.Empty; + public string ReplyTo { get; init; } = string.Empty; public string Subject { get; init; } = string.Empty; public string TextBody { get; init; } = string.Empty; public string HtmlBody { get; init; } = string.Empty; public List Attachments { get; init; } = new(); + + public string? ProviderKey { get; set; } } \ No newline at end of file diff --git a/Core.Email.Abstractions/CoreEmailStatus.cs b/Core.Email.Abstractions/CoreEmailStatus.cs index 312fd47..ec2fea2 100644 --- a/Core.Email.Abstractions/CoreEmailStatus.cs +++ b/Core.Email.Abstractions/CoreEmailStatus.cs @@ -3,6 +3,7 @@ public class CoreEmailStatus { public Guid Id { get; set; } + public string? ProviderMessageId { get; set; } public bool IsSuccess { get; set; } public string Error { get; set; } = string.Empty; } \ No newline at end of file diff --git a/Core.Email.Abstractions/ICoreEmailPersistence.cs b/Core.Email.Abstractions/ICoreEmailPersistence.cs index a200b97..cad5e34 100644 --- a/Core.Email.Abstractions/ICoreEmailPersistence.cs +++ b/Core.Email.Abstractions/ICoreEmailPersistence.cs @@ -6,5 +6,5 @@ public interface ICoreEmailPersistence public Task> GetUnsentAsync(CancellationToken cancellationToken = default); - public Task UpdateStatus(IDictionary updates, CancellationToken cancellationToken = default); + public Task UpdateStatusAsync(IDictionary updates, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/Core.Email.Provider.Mailjet/MailjetProvider.cs b/Core.Email.Provider.Mailjet/MailjetProvider.cs index e7c6c56..e5d42d1 100644 --- a/Core.Email.Provider.Mailjet/MailjetProvider.cs +++ b/Core.Email.Provider.Mailjet/MailjetProvider.cs @@ -30,6 +30,7 @@ public async Task> SendBatchAsync(List m To = x.To.Select(y => new SendContact(y)).ToList(), Cc = x.Cc.Select(y => new SendContact(y)).ToList(), Bcc = x.Bcc.Select(y => new SendContact(y)).ToList(), + ReplyTo = string.IsNullOrEmpty(x.ReplyTo) ? null : new SendContact(x.ReplyTo), Subject = x.Subject, TextPart = x.TextBody, HTMLPart = x.HtmlBody, diff --git a/Core.Email.Provider.Postmark/PostmarkProvider.cs b/Core.Email.Provider.Postmark/PostmarkProvider.cs index 4035ecb..d7e3bf7 100644 --- a/Core.Email.Provider.Postmark/PostmarkProvider.cs +++ b/Core.Email.Provider.Postmark/PostmarkProvider.cs @@ -29,6 +29,7 @@ public async Task> SendBatchAsync(List m To = x.To.First(), Cc = x.Cc.FirstOrDefault(), // TODO: only one? Bcc = x.Bcc.FirstOrDefault(), + ReplyTo = x.ReplyTo, Subject = x.Subject, TextBody = x.TextBody, HtmlBody = x.HtmlBody, @@ -41,9 +42,10 @@ public async Task> SendBatchAsync(List m }).ToList() })).ConfigureAwait(false); - return res.Select(x => new CoreEmailStatus + return res.Select((x, idx) => new CoreEmailStatus { - Id = x.MessageID, // TODO: match order? + Id = messages[idx].Id, + ProviderMessageId = x.MessageID.ToString("N"), IsSuccess = x.Status == PostmarkStatus.Success, Error = x.Message }).ToList(); diff --git a/Core.Email.Provider.SES/SimpleEmailServiceProvider.cs b/Core.Email.Provider.SES/SimpleEmailServiceProvider.cs index 3c9f8df..1a65857 100644 --- a/Core.Email.Provider.SES/SimpleEmailServiceProvider.cs +++ b/Core.Email.Provider.SES/SimpleEmailServiceProvider.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MimeKit; +using System.Net.Mail; namespace Core.Email.Provider.SES; @@ -36,6 +37,9 @@ public async Task> SendBatchAsync(List m var m = new MimeMessage(); m.From.Add(new MailboxAddress("", message.From)); + if (!string.IsNullOrEmpty(message.ReplyTo)) + m.ReplyTo.Add(new MailboxAddress(string.Empty, message.ReplyTo)); + foreach (var to in message.To) m.To.Add(new MailboxAddress(string.Empty, to)); @@ -70,6 +74,7 @@ public async Task> SendBatchAsync(List m list.Add(new CoreEmailStatus { Id = message.Id, + ProviderMessageId = res.MessageId, IsSuccess = (int)res.HttpStatusCode >= 200 && (int)res.HttpStatusCode < 300, Error = string.Empty // TODO: ? }); diff --git a/Core.Email.Provider.SMTP/SmtpProvider.cs b/Core.Email.Provider.SMTP/SmtpProvider.cs index d9e498d..45b911b 100644 --- a/Core.Email.Provider.SMTP/SmtpProvider.cs +++ b/Core.Email.Provider.SMTP/SmtpProvider.cs @@ -37,6 +37,9 @@ await client.ConnectAsync(_options.Host, _options.Port, var m = new MimeMessage(); m.From.Add(new MailboxAddress(string.Empty, message.From)); + if (!string.IsNullOrEmpty(message.ReplyTo)) + m.ReplyTo.Add(new MailboxAddress(string.Empty, message.ReplyTo)); + foreach (var to in message.To) m.To.Add(new MailboxAddress(string.Empty, to)); diff --git a/Core.Email.Provider.SendGrid/SendGridProvider.cs b/Core.Email.Provider.SendGrid/SendGridProvider.cs index 4905eb8..428e54d 100644 --- a/Core.Email.Provider.SendGrid/SendGridProvider.cs +++ b/Core.Email.Provider.SendGrid/SendGridProvider.cs @@ -33,6 +33,9 @@ public async Task> SendBatchAsync(List m new EmailAddress(message.To.First()), message.Subject, message.TextBody, message.HtmlBody); + if (!string.IsNullOrEmpty(message.ReplyTo)) + m.ReplyTo = new EmailAddress(message.ReplyTo); + // TODO: add other Tos foreach (var cc in message.Cc) m.AddCc(new EmailAddress(cc)); @@ -50,7 +53,8 @@ public async Task> SendBatchAsync(List m { Id = message.Id, IsSuccess = res.IsSuccessStatusCode, - Error = await res.Body.ReadAsStringAsync(CancellationToken.None).ConfigureAwait(false) + Error = await res.Body.ReadAsStringAsync(CancellationToken.None).ConfigureAwait(false), + }); } catch (Exception e) diff --git a/Core.Email.Tests/EmailTest.cs b/Core.Email.Tests/EmailTest.cs index 1de793d..9b3230a 100644 --- a/Core.Email.Tests/EmailTest.cs +++ b/Core.Email.Tests/EmailTest.cs @@ -1,3 +1,4 @@ +using System.Text; using Core.Email.Abstractions; using Core.Email.Provider.Mailjet; using Core.Email.Provider.Postmark; @@ -36,12 +37,20 @@ public async Task Test1() var from = config["TestSetup:From"]; var to = config["TestSetup:To"]; - await email.SendAsync(new CoreEmailMessage + var res = await email.SendAsync(new CoreEmailMessage { To = [to!], From = from!, - Subject = "Transactional Mail Test 3", - TextBody = "Transactional Mail Test 3" + Subject = "Transactional Mail Test 5", + TextBody = "Transactional Mail Test 5", + Attachments = [new CoreEmailAttachment + { + Name = "File.txt", + ContentType = "text/plain", + Content = "Hello World!"u8.ToArray() + }] }); + + Assert.True(res.IsSuccess); } } \ No newline at end of file diff --git a/Core.Email/CoreEmailService.cs b/Core.Email/CoreEmailService.cs index 8b12b98..615acc8 100644 --- a/Core.Email/CoreEmailService.cs +++ b/Core.Email/CoreEmailService.cs @@ -7,29 +7,30 @@ namespace Core.Email; internal class CoreEmailService(IServiceProvider serviceProvider, IConfiguration config) : BackgroundService, ICoreEmail { - public ICoreEmailProvider? Provider { get; init; } = - serviceProvider.GetKeyedService(config["Email:Default"]); + private ICoreEmailProvider? _defaultProvider = serviceProvider.GetKeyedService(config["Email:Default"]); - public ICoreEmailPersistence? Persistence { get; init; } + private ICoreEmailPersistence? _persistence = serviceProvider.GetService(); public async Task SendAsync(CoreEmailMessage message, CancellationToken cancellationToken = default) { - if (Provider == null) - throw new InvalidOperationException("default provider not found"); + var provider = message.ProviderKey != null ? serviceProvider.GetKeyedService(message.ProviderKey) : _defaultProvider; - if (Persistence != null) + if (provider == null) + throw new InvalidOperationException($"provider \"{message.ProviderKey ?? "Default"}\" not found"); + + if (_persistence != null) { - await Persistence.StoreBatchAsync([message], cancellationToken); - return new CoreEmailStatus { Id = message.Id, IsSuccess = true }; // TODO: ? + await _persistence.StoreBatchAsync([message], cancellationToken); + return new CoreEmailStatus { Id = message.Id, IsSuccess = true }; } - return (await Provider.SendBatchAsync([message], cancellationToken)).First(); + return (await provider.SendBatchAsync([message], cancellationToken)).First(); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - if (Persistence == null || Provider == null) + if (_persistence == null) return; while (!stoppingToken.IsCancellationRequested) @@ -39,16 +40,25 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) if (stoppingToken.IsCancellationRequested) break; - // TODO: redis lock - try { - var messages = await Persistence.GetUnsentAsync(CancellationToken.None); - await Provider.SendBatchAsync(messages, CancellationToken.None); + var messages = await _persistence.GetUnsentAsync(CancellationToken.None); + foreach (var grouping in messages.GroupBy(x=>x.ProviderKey)) + { + var key = grouping.Key; + var provider = key != null ? serviceProvider.GetKeyedService(key) : _defaultProvider; + + if (provider == null) + continue; + + var res = await provider.SendBatchAsync(messages, CancellationToken.None); + var updates = res.ToDictionary(x => x.Id, x => x.IsSuccess ? null : x.Error); + await _persistence.UpdateStatusAsync(updates, CancellationToken.None); + } } catch (Exception e) { - // + // TODO: } } } diff --git a/README.md b/README.md index 11ae78b..982b5e0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ dotnet add package Core.Email.Provider.SES ``` # .NET Transactional E-Mail Abstraction Layer -- open source (Apache 2.0) - common providers - smtp via mailkit - aws ses @@ -15,7 +14,6 @@ dotnet add package Core.Email.Provider.SES - sendgrid - postmark - TODO - - attachments - bounce handlers # Usage @@ -68,6 +66,12 @@ await email.SendAsync(new CoreEmailMessage To = ["test@example.com"], From = "test@example.com", Subject = "Transactional Mail Subject", - TextBody = "Transactional Mail Body" + TextBody = "Transactional Mail Body", + Attachments = [new CoreEmailAttachment + { + Name = "File.txt", + ContentType = "text/plain", + Content = "Hello World!"u8.ToArray() + }] }); ```