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/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/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 @@
+
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);
+ }
+ }
+}