diff --git a/Directory.Packages.props b/Directory.Packages.props index 919f99e..27b9914 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,6 +5,7 @@ + diff --git a/OneBeyond.Studio.EmailProviders.sln b/OneBeyond.Studio.EmailProviders.sln index 74bce58..cdbc722 100644 --- a/OneBeyond.Studio.EmailProviders.sln +++ b/OneBeyond.Studio.EmailProviders.sln @@ -31,6 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OneBeyond.Studio.EmailProvi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OneBeyond.Studio.EmailProviders.SendGrid.Tests", "src\OneBeyond.Studio.EmailProviders.SendGrid.Tests\OneBeyond.Studio.EmailProviders.SendGrid.Tests.csproj", "{2FB1D7DD-6A61-4377-8C5F-F7FE07D4C729}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneBeyond.Studio.EmailProviders.AwsSes", "src\OneBeyond.Studio.EmailProviders.AwsSes\OneBeyond.Studio.EmailProviders.AwsSes.csproj", "{8C05577B-5225-4C86-9552-B00E3EA88AAD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -77,6 +79,10 @@ Global {2FB1D7DD-6A61-4377-8C5F-F7FE07D4C729}.Debug|Any CPU.Build.0 = Debug|Any CPU {2FB1D7DD-6A61-4377-8C5F-F7FE07D4C729}.Release|Any CPU.ActiveCfg = Release|Any CPU {2FB1D7DD-6A61-4377-8C5F-F7FE07D4C729}.Release|Any CPU.Build.0 = Release|Any CPU + {8C05577B-5225-4C86-9552-B00E3EA88AAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C05577B-5225-4C86-9552-B00E3EA88AAD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C05577B-5225-4C86-9552-B00E3EA88AAD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C05577B-5225-4C86-9552-B00E3EA88AAD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/OneBeyond.Studio.EmailProviders.AwsSes/DependencyInjection/ServiceCollectionExtensions.cs b/src/OneBeyond.Studio.EmailProviders.AwsSes/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..ddce7ee --- /dev/null +++ b/src/OneBeyond.Studio.EmailProviders.AwsSes/DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,36 @@ + +using EnsureThat; +using Microsoft.Extensions.DependencyInjection; +using OneBeyond.Studio.EmailProviders.Domain; +using OneBeyond.Studio.EmailProviders.AwsSes.Options; + +namespace OneBeyond.Studio.EmailProviders.AwsSes.DependencyInjection; + +/// +/// +public static class ServiceCollectionExtensions +{ + /// + /// Configures email sending capabilities on DI container. + /// + /// + /// Email sending configuration options + /// + public static IServiceCollection AddEmailSender(this IServiceCollection @this, EmailSenderOptions emailSenderOptions) + { + EnsureArg.IsNotNull(@this, nameof(@this)); + EnsureArg.IsNotNull(emailSenderOptions, nameof(emailSenderOptions)); + + @this.AddSingleton( + (serviceProvider) => + { + return new EmailSender( + emailSenderOptions.FromEmailAddress, + emailSenderOptions.UseEnforcedToEmailAddress + ? emailSenderOptions.EnforcedToEmailAddress + : default + ); + }); + return @this; + } +} diff --git a/src/OneBeyond.Studio.EmailProviders.AwsSes/EmailSender.cs b/src/OneBeyond.Studio.EmailProviders.AwsSes/EmailSender.cs new file mode 100644 index 0000000..97b68e8 --- /dev/null +++ b/src/OneBeyond.Studio.EmailProviders.AwsSes/EmailSender.cs @@ -0,0 +1,106 @@ +using Amazon.Runtime.Internal; +using Amazon.SimpleEmailV2; +using Amazon.SimpleEmailV2.Model; +using EnsureThat; +using OneBeyond.Studio.EmailProviders.Domain; +using OneBeyond.Studio.EmailProviders.Domain.Exceptions; +using System.Net.Mail; + +namespace OneBeyond.Studio.EmailProviders.AwsSes; + +/// +/// Email Sender for Amazon SES (Simple Email Service). This utilises SES V2 +/// API. Currently we only support SSO/IAM based authentication. This could be +/// extended in future to support AWS Access Key/Secret authentication +/// +internal sealed class EmailSender : IEmailSender +{ + private readonly AmazonSimpleEmailServiceV2Client _emailClient; + private readonly string _defaultFromAddress; + private readonly string? _enforcedToEmailAddress; + + public EmailSender( + string defaultFromAddress, + string? enforcedToEmailAddress) + { + // Picks up profile from appsettings + _emailClient = new AmazonSimpleEmailServiceV2Client(); + _defaultFromAddress = defaultFromAddress; + _enforcedToEmailAddress = enforcedToEmailAddress; + } + + public async Task SendEmailAsync(MailMessage mailMessage, CancellationToken cancellationToken = default) + { + EnsureArg.IsNotNull(mailMessage, nameof(mailMessage)); + + SendEmailRequest ser = new SendEmailRequest(); + + ser.FromEmailAddress = mailMessage.From?.Address ?? _defaultFromAddress; + + var toAddressesList = string.IsNullOrEmpty(_enforcedToEmailAddress) + ? mailMessage.To.Select(x => x.Address).ToList() + : _enforcedToEmailAddress.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList(); + + + ser.Destination = new Destination() + { + ToAddresses = toAddressesList + }; + + if (string.IsNullOrWhiteSpace(_enforcedToEmailAddress)) + { + ser.Destination.CcAddresses = mailMessage.CC.Select(x => x.Address).ToList(); + ser.Destination.BccAddresses = mailMessage.Bcc.Select(x => x.Address).ToList(); + } + + var body = new Body(); + if (mailMessage.IsBodyHtml) + { + body.Html = new Content { Data = mailMessage.Body }; + } + else + { + body.Text = new Content { Data = mailMessage.Body }; + } + + ser.Content = new EmailContent + { + Simple = new Message + { + Subject = new Content { Data = mailMessage.Subject }, + Body = body + } + }; + + try + { + var response = await _emailClient.SendEmailAsync(ser); + // SES message id + return response.MessageId; + } + catch (AccountSuspendedException ex) + { + throw new EmailSenderException("The account's ability to send email has been permanently restricted.", ex); + } + catch (MailFromDomainNotVerifiedException ex) + { + throw new EmailSenderException("The sending domain is not verified.", ex); + } + catch (MessageRejectedException ex) + { + throw new EmailSenderException("The message content is invalid.", ex); + } + catch (SendingPausedException ex) + { + throw new EmailSenderException("The account's ability to send email is currently paused.", ex); + } + catch (TooManyRequestsException ex) + { + throw new EmailSenderException("Too many requests were made. Please try again later.", ex); + } + catch (Exception ex) + { + throw new EmailSenderException($"An error occurred while sending the email", ex); + } + } +} diff --git a/src/OneBeyond.Studio.EmailProviders.AwsSes/OneBeyond.Studio.EmailProviders.AwsSes.csproj b/src/OneBeyond.Studio.EmailProviders.AwsSes/OneBeyond.Studio.EmailProviders.AwsSes.csproj new file mode 100644 index 0000000..7ce736b --- /dev/null +++ b/src/OneBeyond.Studio.EmailProviders.AwsSes/OneBeyond.Studio.EmailProviders.AwsSes.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/src/OneBeyond.Studio.EmailProviders.AwsSes/Options/EmailSenderOptions.cs b/src/OneBeyond.Studio.EmailProviders.AwsSes/Options/EmailSenderOptions.cs new file mode 100644 index 0000000..7fb979b --- /dev/null +++ b/src/OneBeyond.Studio.EmailProviders.AwsSes/Options/EmailSenderOptions.cs @@ -0,0 +1,8 @@ +namespace OneBeyond.Studio.EmailProviders.AwsSes.Options; +/// +/// Options for email sending +/// +public sealed record EmailSenderOptions : Domain.Options.EmailSenderOptions +{ + +}