diff --git a/FluentEmail.sln b/FluentEmail.sln index 039c50ac..8b62cfb9 100644 --- a/FluentEmail.sln +++ b/FluentEmail.sln @@ -54,6 +54,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub Actions", "GitHub Ac .github\workflows\publish-packages.yml = .github\workflows\publish-packages.yml EndProjectSection 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 @@ -116,6 +118,10 @@ Global {089AADB3-D9DF-4EAF-9D0E-AF343AF310DE}.Debug|Any CPU.Build.0 = Debug|Any CPU {089AADB3-D9DF-4EAF-9D0E-AF343AF310DE}.Release|Any CPU.ActiveCfg = Release|Any CPU {089AADB3-D9DF-4EAF-9D0E-AF343AF310DE}.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 @@ -139,6 +145,7 @@ Global {7A36357D-5CE6-4E90-BE5F-8E51553F6846} = {926C0980-31D9-4449-903F-3C756044C28A} {089AADB3-D9DF-4EAF-9D0E-AF343AF310DE} = {926C0980-31D9-4449-903F-3C756044C28A} {1B6987D8-3785-4A78-8637-40E321CC2877} = {7B3C8C77-C54A-4F9E-A241-676AC01E49BB} + {B7A5D5CF-9804-41CA-BF0A-16D5252CE7A9} = {926C0980-31D9-4449-903F-3C756044C28A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {23736554-5288-4B30-9710-B4D9880BCF0B} diff --git a/README.md b/README.md index 8c681bfc..3fd4f9d1 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Original blog post here for a detailed guide [A complete guide to send email in - [FluentEmail.SendGrid](src/Senders/FluentEmail.SendGrid) - Send email via the SendGrid 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. +- [FluentEmail.MailPace](src/Senders/FluentEmail.MailPace) - Send emails via the [MailPace](https://www.mailpace.com/) REST API. - [FluentEmail.MailerSend](https://github.com/marcoatribeiro/FluentEmail.MailerSend) - Send email via [MailerSend](https://www.mailersend.com/)'s API. ## Basic Usage @@ -166,4 +167,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..13a79109 --- /dev/null +++ b/src/Senders/FluentEmail.MailPace/FluentEmail.MailPace.csproj @@ -0,0 +1,20 @@ + + + + Send emails via MailPace using their REST API + Fluent Email - MailPace + $(PackageTags);mailpace + net6.0 + jcamp.$(AssemblyName) + jcamp.$(AssemblyName) + + + + + + + + + + + \ 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..3cf2f6f6 --- /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..b3659642 --- /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..45ed3b7e --- /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..79271910 --- /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..6ac796e6 --- /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