Skip to content

Commit

Permalink
ses sqs bounce
Browse files Browse the repository at this point in the history
  • Loading branch information
coronabytes committed Jan 21, 2024
1 parent b0a64cc commit d5e8821
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 32 deletions.
12 changes: 12 additions & 0 deletions Core.Email.Abstractions/CoreEmailNotification.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Core.Email.Abstractions;

[Serializable]
public class CoreEmailNotification
{
public string ProviderMessageId { get; set; } = string.Empty;

public CoreEmailNotificationType Type { get; set; }
public DateTimeOffset Timestamp { get; set; }

public List<string> Recipients { get; set; } = new();
}
13 changes: 13 additions & 0 deletions Core.Email.Abstractions/CoreEmailNotificationType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;

namespace Core.Email.Abstractions;

[Serializable]
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CoreEmailNotificationType
{
Unknown,
Bounce,
Complaint,
Delivery
}
1 change: 1 addition & 0 deletions Core.Email.Abstractions/CoreEmailStatus.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Core.Email.Abstractions;

[Serializable]
public class CoreEmailStatus
{
public Guid Id { get; set; }
Expand Down
3 changes: 3 additions & 0 deletions Core.Email.Abstractions/ICoreEmailPersistence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ public interface ICoreEmailPersistence
public Task<List<CoreEmailMessage>> GetUnsentAsync(CancellationToken cancellationToken = default);

public Task UpdateStatusAsync(IDictionary<Guid, string?> updates, CancellationToken cancellationToken = default);

public Task StoreNotificationBatchAsync(IList<CoreEmailNotification> notifications,
CancellationToken cancellationToken = default);
}
1 change: 1 addition & 0 deletions Core.Email.Provider.SES/Core.Email.Provider.SES.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

<ItemGroup>
<PackageReference Include="AWSSDK.SimpleEmailV2" Version="3.7.300.39" />
<PackageReference Include="AWSSDK.SQS" Version="3.7.300.39" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
</ItemGroup>

Expand Down
163 changes: 145 additions & 18 deletions Core.Email.Provider.SES/SimpleEmailServiceProvider.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,37 @@
using Amazon;
using System.Text.Json;
using Amazon;
using Amazon.SimpleEmailV2;
using Amazon.SimpleEmailV2.Model;
using Amazon.SQS;
using Amazon.SQS.Model;
using Core.Email.Abstractions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MimeKit;
using System.Net.Mail;

namespace Core.Email.Provider.SES;

internal class SimpleEmailServiceProvider : ICoreEmailProvider
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};

private readonly Options _options = new();
private readonly ICoreEmailPersistence? _persistence;

private readonly AmazonSimpleEmailServiceV2Client _ses;

public SimpleEmailServiceProvider(IConfiguration configuration, [ServiceKey] string key)

public SimpleEmailServiceProvider(IConfiguration configuration, [ServiceKey] string key,
IServiceProvider serviceProvider)
{
configuration.Bind($"Email:{key}", _options);
_ses = new AmazonSimpleEmailServiceV2Client(_options.AccessKey, _options.SecretAccessKey,
RegionEndpoint.GetBySystemName(_options.Region ?? "eu-central-1"));

_persistence = serviceProvider.GetService<ICoreEmailPersistence>();
}

public string Name => "SES";
Expand Down Expand Up @@ -68,23 +80,23 @@ public async Task<List<CoreEmailStatus>> SendBatchAsync(List<CoreEmailMessage> m
stream.Position = 0;

var res = await _ses.SendEmailAsync(new SendEmailRequest
{
FromEmailAddress = message.From,
Destination = new Destination
{
FromEmailAddress = message.From,
Destination = new Destination
{
ToAddresses = message.To,
CcAddresses = message.Cc,
BccAddresses = message.Bcc
},
Content = new EmailContent
ToAddresses = message.To,
CcAddresses = message.Cc,
BccAddresses = message.Bcc
},
Content = new EmailContent
{
Raw = new RawMessage
{
Raw = new RawMessage
{
Data = stream
}
},
ReplyToAddresses = string.IsNullOrEmpty(message.ReplyTo) ? new () : [message.ReplyTo]
}, cancellationToken).ConfigureAwait(false);
Data = stream
}
},
ReplyToAddresses = string.IsNullOrEmpty(message.ReplyTo) ? new List<string>() : [message.ReplyTo]
}, cancellationToken).ConfigureAwait(false);

list.Add(new CoreEmailStatus
{
Expand All @@ -107,11 +119,126 @@ public async Task<List<CoreEmailStatus>> SendBatchAsync(List<CoreEmailMessage> m
return list;
}

public async Task GetNotificationsAsync(CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(_options.QueueUrl) || _persistence == null)
return;

var sqs = new AmazonSQSClient(_options.AccessKey, _options.SecretAccessKey,
RegionEndpoint.GetBySystemName(_options.Region ?? "eu-central-1"));

while (!cancellationToken.IsCancellationRequested)
try
{
var messages = await sqs.ReceiveMessageAsync(new ReceiveMessageRequest
{
MaxNumberOfMessages = 10,
VisibilityTimeout = 60,
QueueUrl = _options.QueueUrl,
WaitTimeSeconds = 30
}, cancellationToken).ConfigureAwait(false);

await _persistence.StoreNotificationBatchAsync(messages.Messages.Select(x =>
{
var body = JsonSerializer.Deserialize<Notification>(x.Body, JsonOptions);

return new CoreEmailNotification
{
ProviderMessageId = body?.Mail?.MessageId ?? string.Empty,
Type = body?.NotificationType switch
{
"Bounce" => CoreEmailNotificationType.Bounce,
"Complaint" => CoreEmailNotificationType.Complaint,
"Delivery" => CoreEmailNotificationType.Delivery,
_ => CoreEmailNotificationType.Unknown
},
Recipients = body?.NotificationType switch
{
"Bounce" => body.Bounce?.BouncedRecipients.Select(y => y.EmailAddress).ToList(),
"Complaint" => body.Complaint?.ComplainedRecipients.Select(y => y.EmailAddress).ToList(),
"Delivery" => body.Delivery?.Recipients.ToList(),
_ => null
} ?? [],
Timestamp = body?.NotificationType switch
{
"Bounce" => body.Bounce?.Timestamp ?? DateTimeOffset.UtcNow,
"Complaint" => body.Complaint?.Timestamp ?? DateTimeOffset.UtcNow,
"Delivery" => body.Delivery?.Timestamp ?? DateTimeOffset.UtcNow,
_ => DateTimeOffset.UtcNow
}
};
}).ToList(), CancellationToken.None);

await sqs.DeleteMessageBatchAsync(new DeleteMessageBatchRequest
{
QueueUrl = _options.QueueUrl,
Entries = messages.Messages
.Select(x => new DeleteMessageBatchRequestEntry(x.MessageId, x.ReceiptHandle))
.ToList()
}, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception e)

Check warning on line 180 in Core.Email.Provider.SES/SimpleEmailServiceProvider.cs

View workflow job for this annotation

GitHub Actions / build

The variable 'e' is declared but never used

Check warning on line 180 in Core.Email.Provider.SES/SimpleEmailServiceProvider.cs

View workflow job for this annotation

GitHub Actions / build

The variable 'e' is declared but never used
{
// TODO:
}
}

[Serializable]
private class Options
{
public string AccessKey { get; set; } = string.Empty;
public string SecretAccessKey { get; set; } = string.Empty;
public string? Region { get; set; }
public string? QueueUrl { get; set; }
}

[Serializable]
private class Notification
{
public string NotificationType { get; set; } = string.Empty;
public BounceNotification? Bounce { get; set; }
public ComplaintNotification? Complaint { get; set; }
public DeliveryNotification? Delivery { get; set; }
public MailNotification? Mail { get; set; }
}

[Serializable]
private class MailNotification
{
public string MessageId { get; set; } = string.Empty;

public DateTimeOffset Timestamp { get; set; }
}

[Serializable]
private class BounceEmail
{
public string EmailAddress { get; set; } = string.Empty;
}

[Serializable]
private class BounceNotification
{
public string BounceType { get; set; } = string.Empty;
public string BounceSubType { get; set; } = string.Empty;

public List<BounceEmail> BouncedRecipients { get; set; } = new();
public DateTimeOffset Timestamp { get; set; }
}

[Serializable]
private class ComplaintNotification
{
public string ComplaintFeedbackType { get; set; } = string.Empty;
public List<BounceEmail> ComplainedRecipients { get; set; } = new();
public DateTimeOffset ArrivalDate { get; set; }
public DateTimeOffset Timestamp { get; set; }
}

[Serializable]
private class DeliveryNotification
{
public List<string> Recipients { get; set; } = new();
public DateTimeOffset Timestamp { get; set; }
}
}
3 changes: 1 addition & 2 deletions Core.Email.Provider.SendGrid/SendGridProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ 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
16 changes: 9 additions & 7 deletions Core.Email.Tests/EmailTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Text;
using Core.Email.Abstractions;
using Core.Email.Provider.Mailjet;
using Core.Email.Provider.Postmark;
Expand Down Expand Up @@ -43,12 +42,15 @@ public async Task Test1()
From = from!,
Subject = "Transactional Mail Test 5",
TextBody = "Transactional Mail Test 5",
Attachments = [new CoreEmailAttachment
{
Name = "File.txt",
ContentType = "text/plain",
Content = "Hello World!"u8.ToArray()
}]
Attachments =
[
new CoreEmailAttachment
{
Name = "File.txt",
ContentType = "text/plain",
Content = "Hello World!"u8.ToArray()
}
]
});

Assert.True(res.IsSuccess);
Expand Down
15 changes: 10 additions & 5 deletions Core.Email/CoreEmailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ namespace Core.Email;

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

private ICoreEmailPersistence? _persistence = serviceProvider.GetService<ICoreEmailPersistence>();
private readonly ICoreEmailPersistence? _persistence = serviceProvider.GetService<ICoreEmailPersistence>();

public async Task<CoreEmailStatus> SendAsync(CoreEmailMessage message,
CancellationToken cancellationToken = default)
{
var provider = message.ProviderKey != null ? serviceProvider.GetKeyedService<ICoreEmailProvider>(message.ProviderKey) : _defaultProvider;
var provider = message.ProviderKey != null
? serviceProvider.GetKeyedService<ICoreEmailProvider>(message.ProviderKey)
: _defaultProvider;

if (provider == null)
throw new InvalidOperationException($"provider \"{message.ProviderKey ?? "Default"}\" not found");
Expand Down Expand Up @@ -43,10 +46,12 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
try
{
var messages = await _persistence.GetUnsentAsync(CancellationToken.None);
foreach (var grouping in messages.GroupBy(x=>x.ProviderKey))
foreach (var grouping in messages.GroupBy(x => x.ProviderKey))
{
var key = grouping.Key;
var provider = key != null ? serviceProvider.GetKeyedService<ICoreEmailProvider>(key) : _defaultProvider;
var provider = key != null
? serviceProvider.GetKeyedService<ICoreEmailProvider>(key)
: _defaultProvider;

if (provider == null)
continue;
Expand Down

0 comments on commit d5e8821

Please sign in to comment.