Skip to content

Commit

Permalink
replyto
Browse files Browse the repository at this point in the history
  • Loading branch information
coronabytes committed Jan 18, 2024
1 parent d66f83b commit 017faf6
Show file tree
Hide file tree
Showing 11 changed files with 67 additions and 25 deletions.
3 changes: 3 additions & 0 deletions Core.Email.Abstractions/CoreEmailMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ public class CoreEmailMessage
public List<string> Cc { get; init; } = new();
public List<string> 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<CoreEmailAttachment> Attachments { get; init; } = new();

public string? ProviderKey { get; set; }
}
1 change: 1 addition & 0 deletions Core.Email.Abstractions/CoreEmailStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion Core.Email.Abstractions/ICoreEmailPersistence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ public interface ICoreEmailPersistence

public Task<List<CoreEmailMessage>> GetUnsentAsync(CancellationToken cancellationToken = default);

public Task UpdateStatus(IDictionary<Guid, string> updates, CancellationToken cancellationToken = default);
public Task UpdateStatusAsync(IDictionary<Guid, string?> updates, CancellationToken cancellationToken = default);
}
1 change: 1 addition & 0 deletions Core.Email.Provider.Mailjet/MailjetProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public async Task<List<CoreEmailStatus>> SendBatchAsync(List<CoreEmailMessage> 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,
Expand Down
6 changes: 4 additions & 2 deletions Core.Email.Provider.Postmark/PostmarkProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public async Task<List<CoreEmailStatus>> SendBatchAsync(List<CoreEmailMessage> 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,
Expand All @@ -41,9 +42,10 @@ public async Task<List<CoreEmailStatus>> SendBatchAsync(List<CoreEmailMessage> 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();
Expand Down
5 changes: 5 additions & 0 deletions Core.Email.Provider.SES/SimpleEmailServiceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MimeKit;
using System.Net.Mail;

namespace Core.Email.Provider.SES;

Expand Down Expand Up @@ -36,6 +37,9 @@ public async Task<List<CoreEmailStatus>> SendBatchAsync(List<CoreEmailMessage> 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));

Expand Down Expand Up @@ -70,6 +74,7 @@ public async Task<List<CoreEmailStatus>> SendBatchAsync(List<CoreEmailMessage> m
list.Add(new CoreEmailStatus
{
Id = message.Id,
ProviderMessageId = res.MessageId,
IsSuccess = (int)res.HttpStatusCode >= 200 && (int)res.HttpStatusCode < 300,
Error = string.Empty // TODO: ?
});
Expand Down
3 changes: 3 additions & 0 deletions Core.Email.Provider.SMTP/SmtpProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down
6 changes: 5 additions & 1 deletion Core.Email.Provider.SendGrid/SendGridProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ public async Task<List<CoreEmailStatus>> SendBatchAsync(List<CoreEmailMessage> 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));
Expand All @@ -50,7 +53,8 @@ public async Task<List<CoreEmailStatus>> SendBatchAsync(List<CoreEmailMessage> 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)
Expand Down
15 changes: 12 additions & 3 deletions Core.Email.Tests/EmailTest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text;
using Core.Email.Abstractions;
using Core.Email.Provider.Mailjet;
using Core.Email.Provider.Postmark;
Expand Down Expand Up @@ -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);
}
}
40 changes: 25 additions & 15 deletions Core.Email/CoreEmailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,30 @@ namespace Core.Email;

internal class CoreEmailService(IServiceProvider serviceProvider, IConfiguration config) : BackgroundService, ICoreEmail
{
public ICoreEmailProvider? Provider { get; init; } =
serviceProvider.GetKeyedService<ICoreEmailProvider>(config["Email:Default"]);
private ICoreEmailProvider? _defaultProvider = serviceProvider.GetKeyedService<ICoreEmailProvider>(config["Email:Default"]);

public ICoreEmailPersistence? Persistence { get; init; }
private ICoreEmailPersistence? _persistence = serviceProvider.GetService<ICoreEmailPersistence>();

public async Task<CoreEmailStatus> SendAsync(CoreEmailMessage message,
CancellationToken cancellationToken = default)
{
if (Provider == null)
throw new InvalidOperationException("default provider not found");
var provider = message.ProviderKey != null ? serviceProvider.GetKeyedService<ICoreEmailProvider>(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)
Expand All @@ -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<ICoreEmailProvider>(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)

Check warning on line 59 in Core.Email/CoreEmailService.cs

View workflow job for this annotation

GitHub Actions / build

The variable 'e' is declared but never used

Check warning on line 59 in Core.Email/CoreEmailService.cs

View workflow job for this annotation

GitHub Actions / build

The variable 'e' is declared but never used

Check warning on line 59 in Core.Email/CoreEmailService.cs

View workflow job for this annotation

GitHub Actions / build

The variable 'e' is declared but never used

Check warning on line 59 in Core.Email/CoreEmailService.cs

View workflow job for this annotation

GitHub Actions / build

The variable 'e' is declared but never used
{
//
// TODO:
}
}
}
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@ 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
- mailjet
- sendgrid
- postmark
- TODO
- attachments
- bounce handlers

# Usage
Expand Down Expand Up @@ -68,6 +66,12 @@ await email.SendAsync(new CoreEmailMessage
To = ["[email protected]"],
From = "[email protected]",
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()
}]
});
```

0 comments on commit 017faf6

Please sign in to comment.