From af712fd0a0006c5aecb5cfad777e4e532822dd21 Mon Sep 17 00:00:00 2001 From: Bon Cyrus Macalindong Date: Wed, 24 Sep 2025 18:45:05 +0800 Subject: [PATCH 1/2] Support for AWS SES --- .../FluentEmail.SES/FluenEmailSESOptions.cs | 9 + .../FluentEmail.SES/FluentEmail.SES.csproj | 17 ++ .../FluentEmailSESBuilderExtensions.cs | 48 ++++++ src/Senders/FluentEmail.SES/SESSender.cs | 155 ++++++++++++++++++ test/FluentEmail.Core.Tests/SESSenderTests.cs | 124 ++++++++++++++ 5 files changed, 353 insertions(+) create mode 100644 src/Senders/FluentEmail.SES/FluenEmailSESOptions.cs create mode 100644 src/Senders/FluentEmail.SES/FluentEmail.SES.csproj create mode 100644 src/Senders/FluentEmail.SES/FluentEmailSESBuilderExtensions.cs create mode 100644 src/Senders/FluentEmail.SES/SESSender.cs create mode 100644 test/FluentEmail.Core.Tests/SESSenderTests.cs diff --git a/src/Senders/FluentEmail.SES/FluenEmailSESOptions.cs b/src/Senders/FluentEmail.SES/FluenEmailSESOptions.cs new file mode 100644 index 00000000..d951c1c1 --- /dev/null +++ b/src/Senders/FluentEmail.SES/FluenEmailSESOptions.cs @@ -0,0 +1,9 @@ +namespace FluentEmail.SES +{ + public class FluenEmailSESOptions + { + public string AccessKeyId { get; set; } + public string SecretAccessKey { get; set; } + public string RegionEndpoint { get; set; } + } +} diff --git a/src/Senders/FluentEmail.SES/FluentEmail.SES.csproj b/src/Senders/FluentEmail.SES/FluentEmail.SES.csproj new file mode 100644 index 00000000..948f3b1c --- /dev/null +++ b/src/Senders/FluentEmail.SES/FluentEmail.SES.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + + + + + + + + + + + + + diff --git a/src/Senders/FluentEmail.SES/FluentEmailSESBuilderExtensions.cs b/src/Senders/FluentEmail.SES/FluentEmailSESBuilderExtensions.cs new file mode 100644 index 00000000..21941278 --- /dev/null +++ b/src/Senders/FluentEmail.SES/FluentEmailSESBuilderExtensions.cs @@ -0,0 +1,48 @@ +using FluentEmail.Core.Interfaces; +using FluentEmail.SES; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class FluentEmailSESBuilderExtensions + { + /// + /// Adds the SES sender services. + /// + /// The Fluent Email Service builder. + /// The options to configure the SES client. + /// + public static FluentEmailServicesBuilder AddSESSender(this FluentEmailServicesBuilder builder, FluenEmailSESOptions sesClientOptions) + { + builder.Services.Configure(options => + { + options.AccessKeyId = sesClientOptions.AccessKeyId; + options.SecretAccessKey = sesClientOptions.SecretAccessKey; + options.RegionEndpoint = sesClientOptions.RegionEndpoint; + }); + + builder.Services.TryAddSingleton(); + return builder; + } + + /// + /// Adds the SES sender services. + /// + /// The Fluent Email Service builder. + /// The options to configure the SES client. + /// + public static FluentEmailServicesBuilder AddSESSender(this FluentEmailServicesBuilder builder, Action sesClientOptions) + { + builder.Services.AddOptions() + .Configure((options, provider) => + { + sesClientOptions(provider, options); + }); + + builder.Services.TryAddSingleton(); + return builder; + } + } + +} diff --git a/src/Senders/FluentEmail.SES/SESSender.cs b/src/Senders/FluentEmail.SES/SESSender.cs new file mode 100644 index 00000000..38b77aff --- /dev/null +++ b/src/Senders/FluentEmail.SES/SESSender.cs @@ -0,0 +1,155 @@ +using Amazon; +using Amazon.SimpleEmailV2; +using Amazon.SimpleEmailV2.Model; +using FluentEmail.Core; +using FluentEmail.Core.Interfaces; +using FluentEmail.Core.Models; +using Microsoft.Extensions.Options; +using MimeKit; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Attachment = FluentEmail.Core.Models.Attachment; + +namespace FluentEmail.SES +{ + public class SESSender : ISender + { + private readonly AmazonSimpleEmailServiceV2Client _emailClient; + + public SESSender(IOptions options) + { + var config = new AmazonSimpleEmailServiceV2Config() + { + RegionEndpoint = RegionEndpoint.GetBySystemName(options.Value.RegionEndpoint) + }; + + _emailClient = new AmazonSimpleEmailServiceV2Client( + options.Value.AccessKeyId, + options.Value.SecretAccessKey, + config); + } + + public SendResponse Send(IFluentEmail email, CancellationToken? token = null) + { + return SendAsync(email, token).GetAwaiter().GetResult(); + } + + public async Task SendAsync(IFluentEmail email, CancellationToken? token = null) + { + var response = new SendResponse(); + + if (token?.IsCancellationRequested ?? false) + { + response.ErrorMessages.Add("Message was cancelled by cancellation token."); + return response; + } + + try + { + var message = new MimeMessage(); + + if (string.IsNullOrWhiteSpace(email.Data.Subject)) + { + response.ErrorMessages.Add("Subject is missing."); + } + + if (email.Data.ToAddresses.Any(a => !string.IsNullOrWhiteSpace(a.EmailAddress))) + { + message.To.AddRange(email.Data.ToAddresses.Select(ConvertAddress)); + } + + if (email.Data.CcAddresses.Any(a => !string.IsNullOrWhiteSpace(a.EmailAddress))) + { + message.Cc.AddRange(email.Data.CcAddresses.Select(ConvertAddress)); + } + + if (email.Data.BccAddresses.Any(a => !string.IsNullOrWhiteSpace(a.EmailAddress))) + { + message.Bcc.AddRange(email.Data.BccAddresses.Select(ConvertAddress)); + } + + if (email.Data.ReplyToAddresses.Any(a => !string.IsNullOrWhiteSpace(a.EmailAddress))) + { + message.ReplyTo.AddRange(email.Data.ReplyToAddresses.Select(ConvertAddress)); + } + + switch (email.Data.Priority) + { + case Priority.High: + message.Priority = MessagePriority.Urgent; + break; + + case Priority.Normal: + // Do not set anything. + // Leave default values. It means Normal Priority. + break; + + case Priority.Low: + message.Priority = MessagePriority.NonUrgent; + break; + } + + message.From.Add(ConvertAddress(email.Data.FromAddress)); + message.Subject = email.Data.Subject; + + var bodyBuilder = new BodyBuilder(); + if (!string.IsNullOrEmpty(email.Data.PlaintextAlternativeBody)) + { + bodyBuilder.TextBody = email.Data.PlaintextAlternativeBody; + bodyBuilder.HtmlBody = email.Data.Body; + } + else if (!email.Data.IsHtml) + { + bodyBuilder.TextBody = email.Data.Body; + } + else + { + bodyBuilder.HtmlBody = email.Data.Body; + } + + foreach (var attachment in email.Data.Attachments ?? new List()) + { + if (attachment.Data != null) + { + bodyBuilder.Attachments.Add( + attachment.Filename, + attachment.Data, + MimeKit.ContentType.Parse(attachment.ContentType ?? "application/octet-stream") + ); + } + } + + message.Body = bodyBuilder.ToMessageBody(); + + using var messageStream = new MemoryStream(); + await message.WriteToAsync(messageStream); + messageStream.Seek(0, SeekOrigin.Begin); + + var emailResponse = await _emailClient.SendEmailAsync(new SendEmailRequest + { + Content = new EmailContent + { + Raw = new RawMessage() + { + Data = messageStream + } + } + }); + + response.MessageId = emailResponse.MessageId; + } + catch (Exception ex) + { + response.ErrorMessages.Add(ex.Message); + } + + return response; + } + + private MailboxAddress ConvertAddress(Address address) => new MailboxAddress(address.Name, address.EmailAddress); + } +} diff --git a/test/FluentEmail.Core.Tests/SESSenderTests.cs b/test/FluentEmail.Core.Tests/SESSenderTests.cs new file mode 100644 index 00000000..e89880c5 --- /dev/null +++ b/test/FluentEmail.Core.Tests/SESSenderTests.cs @@ -0,0 +1,124 @@ +using FluentEmail.Core; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using System.IO; +using System.Threading.Tasks; + +namespace FluentEmail.SES.Tests +{ + public class SESSenderTests + { + // TODO: Put your SES details here. + const string accessKeyId = ""; + const string regionEndpoint = ""; + const string secretAccessKey = ""; + + const string toEmail = "fluentEmail@mailinator.com"; + const string toName = "FluentEmail Mailinator"; + const string fromEmail = "test@fluentmail.com"; + const string fromName = "SESSender Test"; + + [SetUp] + public void SetUp() + { + var sesClientOptions = Options.Create(new FluenEmailSESOptions() + { + AccessKeyId = accessKeyId, + RegionEndpoint = regionEndpoint, + SecretAccessKey = secretAccessKey + }); + + var sender = new SESSender(sesClientOptions); + Email.DefaultSender = sender; + } + + [Test] + [Ignore("No SES credentials")] + public async Task CanSendEmail() + { + const string subject = "SendMail Test"; + const string body = "This email is testing send mail functionality of SES Sender."; + + var email = Email + .From(fromEmail, fromName) + .To(toEmail, toName) + .Subject(subject) + .Body(body); + + var response = await email.SendAsync(); + + Assert.IsTrue(response.Successful); + } + + + [Test] + [Ignore("No SES credentials")] + public async Task CanSendEmailWithAttachments() + { + const string subject = "SendMail With Attachments Test"; + const string body = "This email is testing the attachment functionality of SES Sender."; + + var stream = new MemoryStream(); + var sw = new StreamWriter(stream); + sw.WriteLine("Hey this is some text in an attachment"); + sw.Flush(); + stream.Seek(0, SeekOrigin.Begin); + + var attachment = new Core.Models.Attachment + { + Data = stream, + ContentType = "text/plain", + Filename = "mailgunTest.txt" + }; + + var email = Email + .From(fromEmail) + .To(toEmail) + .Subject(subject) + .Body(body) + .Attach(attachment); + + var response = await email.SendAsync(); + + Assert.IsTrue(response.Successful); + } + + [Test] + [Ignore("No SES credentials")] + public async Task CanSendHighPriorityEmail() + { + const string subject = "SendMail Test"; + const string body = "This email is testing send mail functionality of SES Sender."; + + var email = Email + .From(fromEmail, fromName) + .To(toEmail, toName) + .Subject(subject) + .Body(body) + .HighPriority(); + + var response = await email.SendAsync(); + + Assert.IsTrue(response.Successful); + } + + [Test] + [Ignore("No SES credentials")] + public async Task CanSendLowPriorityEmail() + { + const string subject = "SendMail Test"; + const string body = "This email is testing send mail functionality of SES Sender."; + + var email = Email + .From(fromEmail, fromName) + .To(toEmail, toName) + .Subject(subject) + .Body(body) + .LowPriority(); + + var response = await email.SendAsync(); + + Assert.IsTrue(response.Successful); + } + } +} From 2e4eab52ee8a6585d891b748ec9d507b30e7bf90 Mon Sep 17 00:00:00 2001 From: Bon Cyrus Macalindong Date: Wed, 24 Sep 2025 18:52:46 +0800 Subject: [PATCH 2/2] Add project references --- FluentEmail.sln | 11 +++++++++-- .../FluentEmail.Core.Tests.csproj | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/FluentEmail.sln b/FluentEmail.sln index 7b3ce08e..86ecc727 100644 --- a/FluentEmail.sln +++ b/FluentEmail.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30011.22 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36429.23 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{6DC215BD-05EF-49A6-ADBE-8AE399952EEC}" EndProject @@ -43,6 +43,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentEmail.Liquid", "src\R EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentEmail.Liquid.Tests", "test\FluentEmail.Liquid.Tests\FluentEmail.Liquid.Tests.csproj", "{C8063CBA-D8F3-467A-A75C-63843F0DE862}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentEmail.SES", "src\Senders\FluentEmail.SES\FluentEmail.SES.csproj", "{86BE04B5-E78B-7808-FF7B-95510C6A1F74}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -97,6 +99,10 @@ Global {C8063CBA-D8F3-467A-A75C-63843F0DE862}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8063CBA-D8F3-467A-A75C-63843F0DE862}.Release|Any CPU.ActiveCfg = Release|Any CPU {C8063CBA-D8F3-467A-A75C-63843F0DE862}.Release|Any CPU.Build.0 = Release|Any CPU + {86BE04B5-E78B-7808-FF7B-95510C6A1F74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {86BE04B5-E78B-7808-FF7B-95510C6A1F74}.Debug|Any CPU.Build.0 = Debug|Any CPU + {86BE04B5-E78B-7808-FF7B-95510C6A1F74}.Release|Any CPU.ActiveCfg = Release|Any CPU + {86BE04B5-E78B-7808-FF7B-95510C6A1F74}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -117,6 +123,7 @@ Global {0C7819AD-BC76-465D-9B2A-BE2DA75042F2} = {926C0980-31D9-4449-903F-3C756044C28A} {17100F47-A555-4756-A25F-4F05EDAFA74E} = {12F031E5-8DDC-40A0-9862-8764A6E190C0} {C8063CBA-D8F3-467A-A75C-63843F0DE862} = {47CB89AC-9615-4FA8-90DE-2D849935C36D} + {86BE04B5-E78B-7808-FF7B-95510C6A1F74} = {926C0980-31D9-4449-903F-3C756044C28A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {23736554-5288-4B30-9710-B4D9880BCF0B} diff --git a/test/FluentEmail.Core.Tests/FluentEmail.Core.Tests.csproj b/test/FluentEmail.Core.Tests/FluentEmail.Core.Tests.csproj index cc26ca67..ee50efe8 100644 --- a/test/FluentEmail.Core.Tests/FluentEmail.Core.Tests.csproj +++ b/test/FluentEmail.Core.Tests/FluentEmail.Core.Tests.csproj @@ -7,6 +7,7 @@ +