Skip to content

Added Azure Communication Email Sender #345

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions FluentEmail.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
205 changes: 205 additions & 0 deletions src/Senders/FluentEmail.Azure.Email/AzureEmailSender.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Initializes a new instance of <see cref="AzureEmailSender"/>
/// </summary>
/// <param name="connectionString">Connection string acquired from the Azure Communication Services resource.</param>
public AzureEmailSender(string connectionString)
{
_emailClient = new EmailClient(connectionString);
}

/// <summary> Initializes a new instance of <see cref="AzureEmailSender"/>.</summary>
/// <param name="connectionString">Connection string acquired from the Azure Communication Services resource.</param>
/// <param name="options">Client option exposing <see cref="ClientOptions.Diagnostics"/>, <see cref="ClientOptions.Retry"/>, <see cref="ClientOptions.Transport"/>, etc.</param>
public AzureEmailSender(string connectionString, EmailClientOptions options)
{
_emailClient = new EmailClient(connectionString, options);
}

/// <summary> Initializes a new instance of <see cref="AzureEmailSender"/>.</summary>
/// <param name="endpoint">The URI of the Azure Communication Services resource.</param>
/// <param name="keyCredential">The <see cref="AzureKeyCredential"/> used to authenticate requests.</param>
/// <param name="options">Client option exposing <see cref="ClientOptions.Diagnostics"/>, <see cref="ClientOptions.Retry"/>, <see cref="ClientOptions.Transport"/>, etc.</param>
public AzureEmailSender(Uri endpoint, AzureKeyCredential keyCredential, EmailClientOptions options = default)
{
_emailClient = new EmailClient(endpoint, keyCredential, options);
}

/// <summary> Initializes a new instance of <see cref="AzureEmailSender"/>.</summary>
/// <param name="endpoint">The URI of the Azure Communication Services resource.</param>
/// <param name="tokenCredential">The TokenCredential used to authenticate requests, such as DefaultAzureCredential.</param>
/// <param name="options">Client option exposing <see cref="ClientOptions.Diagnostics"/>, <see cref="ClientOptions.Retry"/>, <see cref="ClientOptions.Transport"/>, etc.</param>
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<SendResponse> 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<EmailAddress>();

if(email.Data.ToAddresses.Any())
{
email.Data.ToAddresses.ForEach(r => toRecipients.Add(new EmailAddress(r.EmailAddress, r.Name)));
}

var ccRecipients = new List<EmailAddress>();

if(email.Data.CcAddresses.Any())
{
email.Data.CcAddresses.ForEach(r => ccRecipients.Add(new EmailAddress($"cc{r.EmailAddress}", r.Name)));
}

var bccRecipients = new List<EmailAddress>();

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<string> { "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<string> { "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<string> { "Failed to send email." }
};
}
catch (Exception ex)
{
return new SendResponse
{
ErrorMessages = new List<string> { ex.Message }
};
}
}

private async Task<EmailAttachment> ConvertAttachment(Attachment attachment) =>
new(attachment.Filename, attachment.ContentType,
await GetAttachmentAsBase64String(attachment.Data));

private async Task<string> GetAttachmentAsBase64String(Stream stream)
{
using var ms = new MemoryStream();

await stream.CopyToAsync(ms);

return Convert.ToBase64String(ms.ToArray());
}
}
18 changes: 18 additions & 0 deletions src/Senders/FluentEmail.Azure.Email/FluentEmail.Azure.Email.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>Send emails via Azure Email Communication Services API</Description>
<AssemblyTitle>Fluent Email - Azure Email</AssemblyTitle>
<PackageTags>$(PackageTags);azureemail</PackageTags>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\FluentEmail.Core\FluentEmail.Core.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.Communication.Email" Version="1.0.0-beta.1" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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<ISender>(_ => new AzureEmailSender(connectionString)));
return builder;
}

public static FluentEmailServicesBuilder AddAzureEmailSender(
this FluentEmailServicesBuilder builder,
string connectionString,
EmailClientOptions options)
{
builder.Services.TryAdd(ServiceDescriptor.Scoped<ISender>(_ => 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<ISender>(_ => 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<ISender>(_ => new AzureEmailSender(endpoint, tokenCredential, options)));
return builder;
}
}
}
Loading