diff --git a/FluentEmail.sln b/FluentEmail.sln
index 7b3ce08e..03ea4079 100644
--- a/FluentEmail.sln
+++ b/FluentEmail.sln
@@ -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.Azure.Email", "src\Senders\FluentEmail.Azure.Email\FluentEmail.Azure.Email.csproj", "{7A36357D-5CE6-4E90-BE5F-8E51553F6846}"
+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
+ {7A36357D-5CE6-4E90-BE5F-8E51553F6846}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7A36357D-5CE6-4E90-BE5F-8E51553F6846}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7A36357D-5CE6-4E90-BE5F-8E51553F6846}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7A36357D-5CE6-4E90-BE5F-8E51553F6846}.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}
+ {7A36357D-5CE6-4E90-BE5F-8E51553F6846} = {926C0980-31D9-4449-903F-3C756044C28A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {23736554-5288-4B30-9710-B4D9880BCF0B}
diff --git a/src/Senders/FluentEmail.Azure.Email/AzureEmailSender.cs b/src/Senders/FluentEmail.Azure.Email/AzureEmailSender.cs
new file mode 100644
index 00000000..03e3992e
--- /dev/null
+++ b/src/Senders/FluentEmail.Azure.Email/AzureEmailSender.cs
@@ -0,0 +1,205 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure;
+using Azure.Communication.Email;
+using Azure.Communication.Email.Models;
+using Azure.Core;
+using FluentEmail.Core;
+using FluentEmail.Core.Interfaces;
+using FluentEmail.Core.Models;
+
+namespace FluentEmail.Azure.Email;
+
+// Read more about Azure Email Communication Services here:
+// https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/email/send-email?pivots=programming-language-csharp
+public class AzureEmailSender : ISender
+{
+ private EmailClient _emailClient;
+
+ ///
+ /// Initializes a new instance of
+ ///
+ /// Connection string acquired from the Azure Communication Services resource.
+ public AzureEmailSender(string connectionString)
+ {
+ _emailClient = new EmailClient(connectionString);
+ }
+
+ /// Initializes a new instance of .
+ /// Connection string acquired from the Azure Communication Services resource.
+ /// Client option exposing , , , etc.
+ public AzureEmailSender(string connectionString, EmailClientOptions options)
+ {
+ _emailClient = new EmailClient(connectionString, options);
+ }
+
+ /// Initializes a new instance of .
+ /// The URI of the Azure Communication Services resource.
+ /// The used to authenticate requests.
+ /// Client option exposing , , , etc.
+ public AzureEmailSender(Uri endpoint, AzureKeyCredential keyCredential, EmailClientOptions options = default)
+ {
+ _emailClient = new EmailClient(endpoint, keyCredential, options);
+ }
+
+ /// Initializes a new instance of .
+ /// The URI of the Azure Communication Services resource.
+ /// The TokenCredential used to authenticate requests, such as DefaultAzureCredential.
+ /// Client option exposing , , , etc.
+ public AzureEmailSender(Uri endpoint, TokenCredential tokenCredential, EmailClientOptions options = default)
+ {
+ _emailClient = new EmailClient(endpoint, tokenCredential, options);
+ }
+
+ public SendResponse Send(IFluentEmail email, CancellationToken? token = null)
+ {
+ return SendAsync(email, token).GetAwaiter().GetResult();
+ }
+
+ public async Task SendAsync(IFluentEmail email, CancellationToken? token = null)
+ {
+ var emailContent = new EmailContent(email.Data.Subject);
+
+ if (email.Data.IsHtml)
+ {
+ emailContent.Html = email.Data.Body;
+ }
+ else
+ {
+ emailContent.PlainText = email.Data.Body;
+ }
+
+ var toRecipients = new List();
+
+ if(email.Data.ToAddresses.Any())
+ {
+ email.Data.ToAddresses.ForEach(r => toRecipients.Add(new EmailAddress(r.EmailAddress, r.Name)));
+ }
+
+ var ccRecipients = new List();
+
+ if(email.Data.CcAddresses.Any())
+ {
+ email.Data.CcAddresses.ForEach(r => ccRecipients.Add(new EmailAddress($"cc{r.EmailAddress}", r.Name)));
+ }
+
+ var bccRecipients = new List();
+
+ if(email.Data.BccAddresses.Any())
+ {
+ email.Data.BccAddresses.ForEach(r => bccRecipients.Add(new EmailAddress($"bcc{r.EmailAddress}", r.Name)));
+ }
+
+ var emailRecipients = new EmailRecipients(toRecipients, ccRecipients, bccRecipients);
+
+ var sender = $"{email.Data.FromAddress.Name} <{email.Data.FromAddress.EmailAddress}>";
+ var emailMessage = new EmailMessage(sender, emailContent, emailRecipients);
+
+ if (email.Data.ReplyToAddresses.Any(a => !string.IsNullOrWhiteSpace(a.EmailAddress)))
+ {
+ foreach (var emailAddress in email.Data.ReplyToAddresses)
+ {
+ emailMessage.ReplyTo.Add(new EmailAddress(emailAddress.EmailAddress, emailAddress.Name));
+ }
+ }
+
+ if (email.Data.Headers.Any())
+ {
+ foreach (var header in email.Data.Headers)
+ {
+ emailMessage.CustomHeaders.Add(new EmailCustomHeader(header.Key, header.Value));
+ }
+ }
+
+ if(email.Data.Attachments.Any())
+ {
+ foreach (var attachment in email.Data.Attachments)
+ {
+ emailMessage.Attachments.Add(await ConvertAttachment(attachment));
+ }
+ }
+
+ emailMessage.Importance = email.Data.Priority switch
+ {
+ Priority.High => EmailImportance.High,
+ Priority.Normal => EmailImportance.Normal,
+ Priority.Low => EmailImportance.Low,
+ _ => EmailImportance.Normal
+ };
+
+ try
+ {
+ var sendEmailResult = (await _emailClient.SendAsync(emailMessage, token ?? CancellationToken.None)).Value;
+
+ var messageId = sendEmailResult.MessageId;
+ if (string.IsNullOrWhiteSpace(messageId))
+ {
+ return new SendResponse
+ {
+ ErrorMessages = new List { "Failed to send email." }
+ };
+ }
+
+ // We want to verify that the email was sent.
+ // The maximum time we will wait for the message status to be sent/delivered is 2 minutes.
+ var cancellationToken = new CancellationTokenSource(TimeSpan.FromMinutes(2));
+ SendStatusResult sendStatusResult;
+ do
+ {
+ sendStatusResult = await _emailClient.GetSendStatusAsync(messageId, cancellationToken.Token);
+
+ if (sendStatusResult.Status != SendStatus.Queued)
+ {
+ break;
+ }
+
+ await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken.Token);
+ } while (!cancellationToken.IsCancellationRequested);
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return new SendResponse
+ {
+ ErrorMessages = new List { "Failed to send email, timed out while getting status." }
+ };
+ }
+
+ if (sendStatusResult.Status == SendStatus.OutForDelivery)
+ {
+ return new SendResponse
+ {
+ MessageId = messageId
+ };
+ }
+
+ return new SendResponse
+ {
+ ErrorMessages = new List { "Failed to send email." }
+ };
+ }
+ catch (Exception ex)
+ {
+ return new SendResponse
+ {
+ ErrorMessages = new List { ex.Message }
+ };
+ }
+ }
+
+ private async Task ConvertAttachment(Attachment attachment) =>
+ new(attachment.Filename, attachment.ContentType,
+ await GetAttachmentAsBase64String(attachment.Data));
+
+ private async Task GetAttachmentAsBase64String(Stream stream)
+ {
+ using var ms = new MemoryStream();
+
+ await stream.CopyToAsync(ms);
+
+ return Convert.ToBase64String(ms.ToArray());
+ }
+}
\ No newline at end of file
diff --git a/src/Senders/FluentEmail.Azure.Email/FluentEmail.Azure.Email.csproj b/src/Senders/FluentEmail.Azure.Email/FluentEmail.Azure.Email.csproj
new file mode 100644
index 00000000..1ee9c716
--- /dev/null
+++ b/src/Senders/FluentEmail.Azure.Email/FluentEmail.Azure.Email.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Send emails via Azure Email Communication Services API
+ Fluent Email - Azure Email
+ $(PackageTags);azureemail
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Senders/FluentEmail.Azure.Email/FluentEmailAzureEmailBuilderExtensions.cs b/src/Senders/FluentEmail.Azure.Email/FluentEmailAzureEmailBuilderExtensions.cs
new file mode 100644
index 00000000..044c062d
--- /dev/null
+++ b/src/Senders/FluentEmail.Azure.Email/FluentEmailAzureEmailBuilderExtensions.cs
@@ -0,0 +1,50 @@
+using System;
+using Azure;
+using Azure.Communication.Email;
+using Azure.Core;
+using FluentEmail.Core.Interfaces;
+using FluentEmail.Azure.Email;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public static class FluentEmailAzureEmailBuilderExtensions
+ {
+ public static FluentEmailServicesBuilder AddAzureEmailSender(
+ this FluentEmailServicesBuilder builder,
+ string connectionString)
+ {
+ builder.Services.TryAdd(ServiceDescriptor.Scoped(_ => new AzureEmailSender(connectionString)));
+ return builder;
+ }
+
+ public static FluentEmailServicesBuilder AddAzureEmailSender(
+ this FluentEmailServicesBuilder builder,
+ string connectionString,
+ EmailClientOptions options)
+ {
+ builder.Services.TryAdd(ServiceDescriptor.Scoped(_ => new AzureEmailSender(connectionString, options)));
+ return builder;
+ }
+
+ public static FluentEmailServicesBuilder AddAzureEmailSender(
+ this FluentEmailServicesBuilder builder,
+ Uri endpoint,
+ AzureKeyCredential keyCredential,
+ EmailClientOptions options = default)
+ {
+ builder.Services.TryAdd(ServiceDescriptor.Scoped(_ => new AzureEmailSender(endpoint, keyCredential, options)));
+ return builder;
+ }
+
+ public static FluentEmailServicesBuilder AddAzureEmailSender(
+ this FluentEmailServicesBuilder builder,
+ Uri endpoint,
+ TokenCredential tokenCredential,
+ EmailClientOptions options = default)
+ {
+ builder.Services.TryAdd(ServiceDescriptor.Scoped(_ => new AzureEmailSender(endpoint, tokenCredential, options)));
+ return builder;
+ }
+ }
+}
diff --git a/test/FluentEmail.Core.Tests/AzureEmailSenderTests.cs b/test/FluentEmail.Core.Tests/AzureEmailSenderTests.cs
new file mode 100644
index 00000000..83111935
--- /dev/null
+++ b/test/FluentEmail.Core.Tests/AzureEmailSenderTests.cs
@@ -0,0 +1,126 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using Attachment = FluentEmail.Core.Models.Attachment;
+
+namespace FluentEmail.Azure.Email.Tests
+{
+ [NonParallelizable]
+ public class AzureEmailSenderTests
+ {
+ const string connectionString = ""; // TODO: Put your ConnectionString here
+
+ const string toEmail = "fluentEmail@mailinator.com";
+ const string toName = "FluentEmail Mailinator";
+ const string fromEmail = "test@fluentmail.com"; // TODO: Put a valid/verified sender here
+ const string fromName = "AzureEmailSender Test";
+
+ [SetUp]
+ public void SetUp()
+ {
+ if (string.IsNullOrWhiteSpace(connectionString)) throw new ArgumentException("Azure Communication Services Connection String needs to be supplied");
+
+ var sender = new AzureEmailSender(connectionString);
+ Core.Email.DefaultSender = sender;
+ }
+
+ [Test, Ignore("No azure credentials")]
+ public async Task CanSendEmail()
+ {
+ const string subject = "SendMail Test";
+ const string body = "This email is testing send mail functionality of Azure Email Sender.";
+
+ var email = Core.Email
+ .From(fromEmail, fromName)
+ .To(toEmail, toName)
+ .Subject(subject)
+ .Body(body);
+
+ var response = await email.SendAsync();
+
+ Assert.IsTrue(response.Successful);
+ }
+
+ [Test, Ignore("No azure credentials")]
+ public async Task CanSendEmailWithReplyTo()
+ {
+ const string subject = "SendMail Test";
+ const string body = "This email is testing send mail with ReplyTo functionality of Azure Email Sender.";
+
+ var email = Core.Email
+ .From(fromEmail, fromName)
+ .To(toEmail, toName)
+ .ReplyTo(toEmail, toName)
+ .Subject(subject)
+ .Body(body);
+
+ var response = await email.SendAsync();
+
+ Assert.IsTrue(response.Successful);
+ }
+
+ [Test, Ignore("No azure credentials")]
+ public async Task CanSendEmailWithAttachments()
+ {
+ const string subject = "SendMail With Attachments Test";
+ const string body = "This email is testing the attachment functionality of Azure Email Sender.";
+
+ await using var stream = File.OpenRead($"{Directory.GetCurrentDirectory()}/test-binary.xlsx");
+ var attachment = new Attachment
+ {
+ Data = stream,
+ ContentType = "xlsx",
+ Filename = "test-binary.xlsx"
+ };
+
+ var email = Core.Email
+ .From(fromEmail, fromName)
+ .To(toEmail, toName)
+ .Subject(subject)
+ .Body(body)
+ .Attach(attachment);
+
+
+ var response = await email.SendAsync();
+
+ Assert.IsTrue(response.Successful);
+ }
+
+ [Test, Ignore("No azure credentials")]
+ public async Task CanSendHighPriorityEmail()
+ {
+ const string subject = "SendMail Test";
+ const string body = "This email is testing send mail functionality of Azure Email Sender.";
+
+ var email = Core.Email
+ .From(fromEmail, fromName)
+ .To(toEmail, toName)
+ .Subject(subject)
+ .Body(body)
+ .HighPriority();
+
+ var response = await email.SendAsync();
+
+ Assert.IsTrue(response.Successful);
+ }
+
+ [Test, Ignore("No azure credentials")]
+ public async Task CanSendLowPriorityEmail()
+ {
+ const string subject = "SendMail Test";
+ const string body = "This email is testing send mail functionality of Azure Email Sender.";
+
+ var email = Core.Email
+ .From(fromEmail, fromName)
+ .To(toEmail, toName)
+ .Subject(subject)
+ .Body(body)
+ .LowPriority();
+
+ var response = await email.SendAsync();
+
+ Assert.IsTrue(response.Successful);
+ }
+ }
+}