From 6de3f14ebc5095341e9433ab95e7d56905b63ac8 Mon Sep 17 00:00:00 2001 From: Maarten Balliauw Date: Mon, 20 Nov 2023 16:47:00 +0100 Subject: [PATCH] Add MailPace Sender - https://github.com/lukencode/FluentEmail/issues/377 --- FluentEmail.sln | 7 + README.markdown | 4 +- .../FluentEmail.MailPace.csproj | 19 +++ .../FluentEmailMailPaceBuilderExtensions.cs | 18 +++ .../MailPaceAttachment.cs | 11 ++ .../FluentEmail.MailPace/MailPaceResponse.cs | 12 ++ .../MailPaceSendRequest.cs | 18 +++ .../FluentEmail.MailPace/MailPaceSender.cs | 143 ++++++++++++++++++ .../FluentEmail.MailPace/StreamExtensions.cs | 23 +++ 9 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 src/Senders/FluentEmail.MailPace/FluentEmail.MailPace.csproj create mode 100644 src/Senders/FluentEmail.MailPace/FluentEmailMailPaceBuilderExtensions.cs create mode 100644 src/Senders/FluentEmail.MailPace/MailPaceAttachment.cs create mode 100644 src/Senders/FluentEmail.MailPace/MailPaceResponse.cs create mode 100644 src/Senders/FluentEmail.MailPace/MailPaceSendRequest.cs create mode 100644 src/Senders/FluentEmail.MailPace/MailPaceSender.cs create mode 100644 src/Senders/FluentEmail.MailPace/StreamExtensions.cs diff --git a/FluentEmail.sln b/FluentEmail.sln index 7b3ce08e..00a68067 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.MailPace", "src\Senders\FluentEmail.MailPace\FluentEmail.MailPace.csproj", "{B7A5D5CF-9804-41CA-BF0A-16D5252CE7A9}" +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 + {B7A5D5CF-9804-41CA-BF0A-16D5252CE7A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7A5D5CF-9804-41CA-BF0A-16D5252CE7A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7A5D5CF-9804-41CA-BF0A-16D5252CE7A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7A5D5CF-9804-41CA-BF0A-16D5252CE7A9}.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} + {B7A5D5CF-9804-41CA-BF0A-16D5252CE7A9} = {926C0980-31D9-4449-903F-3C756044C28A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {23736554-5288-4B30-9710-B4D9880BCF0B} diff --git a/README.markdown b/README.markdown index 7717ba80..3be53a00 100644 --- a/README.markdown +++ b/README.markdown @@ -22,6 +22,7 @@ Maintained by Luke Lowrey - follow me on twitter **[@lukencode](https://twitter * [FluentEmail.Mailgun](src/Senders/FluentEmail.Mailgun) - Send emails via MailGun's REST API. * [FluentEmail.SendGrid](src/Senders/FluentEmail.SendGrid) - Send email via the SendGrid API. +* [FluentEmail.MailPace](src/Senders/FluentEmail.MailPace) - Send emails via the [MailPace](https://www.mailpace.com/) REST API. * [FluentEmail.Mailtrap](src/Senders/FluentEmail.Mailtrap) - Send emails to Mailtrap. Uses [FluentEmail.Smtp](src/Senders/FluentEmail.Smtp) for delivery. * [FluentEmail.MailKit](src/Senders/FluentEmail.MailKit) - Send emails using the [MailKit](https://github.com/jstedfast/MailKit) email library. @@ -159,5 +160,4 @@ var email = new Email("bob@hotmail.com") .UsingTemplateFromEmbedded("Example.Project.Namespace.template-name.cshtml", new { Name = "Bob" }, TypeFromYourEmbeddedAssembly.GetType().GetTypeInfo().Assembly); -``` - +``` \ No newline at end of file diff --git a/src/Senders/FluentEmail.MailPace/FluentEmail.MailPace.csproj b/src/Senders/FluentEmail.MailPace/FluentEmail.MailPace.csproj new file mode 100644 index 00000000..dd3dc68a --- /dev/null +++ b/src/Senders/FluentEmail.MailPace/FluentEmail.MailPace.csproj @@ -0,0 +1,19 @@ + + + + Send emails via MailPace using their REST API + Fluent Email - MailPace + Luke Lowrey;Ben Cull;Github Contributors + $(PackageTags);smtp + netstandard2.0 + + + + + + + + + + + \ No newline at end of file diff --git a/src/Senders/FluentEmail.MailPace/FluentEmailMailPaceBuilderExtensions.cs b/src/Senders/FluentEmail.MailPace/FluentEmailMailPaceBuilderExtensions.cs new file mode 100644 index 00000000..6023605e --- /dev/null +++ b/src/Senders/FluentEmail.MailPace/FluentEmailMailPaceBuilderExtensions.cs @@ -0,0 +1,18 @@ +using FluentEmail.Core.Interfaces; +using FluentEmail.MailPace; +using Microsoft.Extensions.DependencyInjection.Extensions; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection +{ + public static class FluentEmailMailPaceBuilderExtensions + { + public static FluentEmailServicesBuilder AddMailPaceSender( + this FluentEmailServicesBuilder builder, + string serverToken) + { + builder.Services.TryAdd(ServiceDescriptor.Scoped(_ => new MailPaceSender(serverToken))); + return builder; + } + } +} \ No newline at end of file diff --git a/src/Senders/FluentEmail.MailPace/MailPaceAttachment.cs b/src/Senders/FluentEmail.MailPace/MailPaceAttachment.cs new file mode 100644 index 00000000..459712c4 --- /dev/null +++ b/src/Senders/FluentEmail.MailPace/MailPaceAttachment.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; + +namespace FluentEmail.MailPace; + +public class MailPaceAttachment +{ + [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("content")] public string Content { get; set; } + [JsonProperty("content_type")] public string ContentType { get; set; } + [JsonProperty("cid", NullValueHandling = NullValueHandling.Ignore)] public string Cid { get; set; } +} \ No newline at end of file diff --git a/src/Senders/FluentEmail.MailPace/MailPaceResponse.cs b/src/Senders/FluentEmail.MailPace/MailPaceResponse.cs new file mode 100644 index 00000000..00ef2c0d --- /dev/null +++ b/src/Senders/FluentEmail.MailPace/MailPaceResponse.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace FluentEmail.MailPace; + +public class MailPaceResponse +{ + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] public string Id { get; set; } + [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] public string Status { get; set; } + [JsonProperty("error")] public string Error { get; set; } + [JsonProperty("errors")] public Dictionary> Errors { get; set; } = new(); +} \ No newline at end of file diff --git a/src/Senders/FluentEmail.MailPace/MailPaceSendRequest.cs b/src/Senders/FluentEmail.MailPace/MailPaceSendRequest.cs new file mode 100644 index 00000000..97cd02de --- /dev/null +++ b/src/Senders/FluentEmail.MailPace/MailPaceSendRequest.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace FluentEmail.MailPace; + +public class MailPaceSendRequest +{ + [JsonProperty("from")] public string From { get; set; } + [JsonProperty("to")] public string To { get; set; } + [JsonProperty("cc", NullValueHandling = NullValueHandling.Ignore)] public string Cc { get; set; } + [JsonProperty("bcc", NullValueHandling = NullValueHandling.Ignore)] public string Bcc { get; set; } + [JsonProperty("subject")] public string Subject { get; set; } + [JsonProperty("htmlbody", NullValueHandling = NullValueHandling.Ignore)] public string HtmlBody { get; set; } + [JsonProperty("textbody", NullValueHandling = NullValueHandling.Ignore)] public string TextBody { get; set; } + [JsonProperty("replyto", NullValueHandling = NullValueHandling.Ignore)] public string ReplyTo { get; set; } + [JsonProperty("attachments")] public List Attachments { get; set; } = new(0); + [JsonProperty("tags")] public List Tags { get; set; } = new(0); +} \ No newline at end of file diff --git a/src/Senders/FluentEmail.MailPace/MailPaceSender.cs b/src/Senders/FluentEmail.MailPace/MailPaceSender.cs new file mode 100644 index 00000000..8e13ad30 --- /dev/null +++ b/src/Senders/FluentEmail.MailPace/MailPaceSender.cs @@ -0,0 +1,143 @@ +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using FluentEmail.Core; +using FluentEmail.Core.Interfaces; +using FluentEmail.Core.Models; +using Newtonsoft.Json; + +namespace FluentEmail.MailPace; + +public class MailPaceSender : ISender, IDisposable +{ + private readonly HttpClient _httpClient; + + public MailPaceSender(string serverToken) + { + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.Add("MailPace-Server-Token", serverToken); + _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + } + + public SendResponse Send(IFluentEmail email, CancellationToken? token = null) => + SendAsync(email, token).GetAwaiter().GetResult(); + + public async Task SendAsync(IFluentEmail email, CancellationToken? token = null) + { + var sendRequest = BuildSendRequestFor(email); + + var content = new StringContent(JsonConvert.SerializeObject(sendRequest), Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync("https://app.mailpace.com/api/v1/send", content) + .ConfigureAwait(false); + + var mailPaceResponse = await TryOrNull(async () => JsonConvert.DeserializeObject( + await response.Content.ReadAsStringAsync())) ?? new MailPaceResponse(); + + if (response.IsSuccessStatusCode) + { + return new SendResponse { MessageId = mailPaceResponse.Id }; + } + else + { + var result = new SendResponse(); + + if (!string.IsNullOrEmpty(mailPaceResponse.Error)) + { + result.ErrorMessages.Add(mailPaceResponse.Error); + } + + if (mailPaceResponse.Errors != null && mailPaceResponse.Errors.Count != 0) + { + result.ErrorMessages.AddRange(mailPaceResponse.Errors + .Select(it => $"{it.Key}: {string.Join("; ", it.Value)}")); + } + + if (!result.ErrorMessages.Any()) + { + result.ErrorMessages.Add(response.ReasonPhrase ?? "An unknown error has occurred."); + } + + return result; + } + } + + private static MailPaceSendRequest BuildSendRequestFor(IFluentEmail email) + { + var sendRequest = new MailPaceSendRequest + { + From = $"{email.Data.FromAddress.Name} <{email.Data.FromAddress.EmailAddress}>", + To = string.Join(",", email.Data.ToAddresses.Select(it => !string.IsNullOrEmpty(it.Name) ? $"{it.Name} <{it.EmailAddress}>" : it.EmailAddress)), + Subject = email.Data.Subject + }; + + if (email.Data.CcAddresses.Any()) + { + sendRequest.Cc = string.Join(",", email.Data.CcAddresses.Select(it => !string.IsNullOrEmpty(it.Name) ? $"{it.Name} <{it.EmailAddress}>" : it.EmailAddress)); + } + + if (email.Data.BccAddresses.Any()) + { + sendRequest.Bcc = string.Join(",", email.Data.BccAddresses.Select(it => !string.IsNullOrEmpty(it.Name) ? $"{it.Name} <{it.EmailAddress}>" : it.EmailAddress)); + } + + if (email.Data.ReplyToAddresses.Any()) + { + sendRequest.ReplyTo = string.Join(",", email.Data.ReplyToAddresses.Select(it => !string.IsNullOrEmpty(it.Name) ? $"{it.Name} <{it.EmailAddress}>" : it.EmailAddress)); + } + + if (email.Data.IsHtml) + { + sendRequest.HtmlBody = email.Data.Body; + if (!string.IsNullOrEmpty(email.Data.PlaintextAlternativeBody)) + { + sendRequest.TextBody = email.Data.PlaintextAlternativeBody; + } + } + else + { + sendRequest.TextBody = email.Data.Body; + } + + if (email.Data.Tags.Any()) + { + sendRequest.Tags.AddRange(email.Data.Tags); + } + + if (email.Data.Attachments.Any()) + { + sendRequest.Attachments.AddRange( + email.Data.Attachments.Select(it => new MailPaceAttachment + { + Name = it.Filename, + Content = it.Data.ConvertToBase64(), + ContentType = it.ContentType ?? Path.GetExtension(it.Filename), // jpeg, jpg, png, gif, txt, pdf, docx, xlsx, pptx, csv, att, ics, ical, html, zip + Cid = it.IsInline ? it.ContentId : null + })); + } + + return sendRequest; + } + + private async Task TryOrNull(Func> method) + { + try + { + return await method(); + } + catch + { + return default; + } + } + + public void Dispose() + { + _httpClient.Dispose(); + } +} \ No newline at end of file diff --git a/src/Senders/FluentEmail.MailPace/StreamExtensions.cs b/src/Senders/FluentEmail.MailPace/StreamExtensions.cs new file mode 100644 index 00000000..420044a3 --- /dev/null +++ b/src/Senders/FluentEmail.MailPace/StreamExtensions.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; + +namespace FluentEmail.MailPace; + +public static class StreamExtensions +{ + public static string ConvertToBase64(this Stream stream) + { + if (stream is MemoryStream memoryStream) + { + return Convert.ToBase64String(memoryStream.ToArray()); + } + + var bytes = new Byte[(int)stream.Length]; + + stream.Seek(0, SeekOrigin.Begin); + // ReSharper disable once MustUseReturnValue + stream.Read(bytes, 0, (int)stream.Length); + + return Convert.ToBase64String(bytes); + } +} \ No newline at end of file