diff --git a/mixtape/Mails/Dispatchers/Lettermint/LettermintDispatcher.cs b/mixtape/Mails/Dispatchers/Lettermint/LettermintDispatcher.cs new file mode 100644 index 00000000..a69e8798 --- /dev/null +++ b/mixtape/Mails/Dispatchers/Lettermint/LettermintDispatcher.cs @@ -0,0 +1,157 @@ +using System.IO; +using System.Net.Http; +using System.Net.Http.Json; +using System.Net.Mail; +using System.Text.Json; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Attachment = System.Net.Mail.Attachment; + +namespace Mixtape.Mails.Dispatchers.Lettermint; + +public class LettermintDispatcher : IMailDispatcher +{ + protected Queue Queue { get; } = new(); + + protected MailOptions Options { get; set; } + + protected IWebHostEnvironment Env { get; set; } + + protected HttpClient Http { get; } + + protected JsonSerializerOptions JsonSerializerOptions { get; } + + protected ILogger Logger { get; } + + + public LettermintDispatcher(IOptionsMonitor monitor, IWebHostEnvironment env, HttpClient http, ILogger logger) + { + Options = monitor.CurrentValue; + Env = env; + Http = http; + Http.DefaultRequestHeaders.Add("x-lettermint-token", Options.Lettermint.Token); + JsonSerializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower + }; + Logger = logger; + + monitor.OnChange(opts => Options = opts); + } + + + /// + public Task IsSenderSupported(string email, CancellationToken token = default) + { + return Task.FromResult(true); + } + + + /// + public async Task Send(Mail message, CancellationToken token = default) + { + string uri = Options.Lettermint.ApiUrl + $"/v1/send"; + + // add app name as tag + if (message.Metadata.TryGetValue("application", out string value) && !message.Tag.HasValue()) + { + message.Tag = value; + message.Metadata.Remove("application"); + } + + LettermintRequest.SendEmail data = new() + { + // to addresses + To = Convert(message.To), + ReplyTo = Convert(message.ReplyToList), + Cc = Convert(message.CC), + Bcc = Convert(message.Bcc), + + // from address + From = message.From!.ToString(), + + // subject + Subject = message.Subject, + + // tag/group + Tag = message.Tag, + + // metadata + Metadata = message.Metadata, + Route = Options.Lettermint.Route + }; + + // set attachments + foreach (Attachment attachment in message.Attachments) + { + data.Attachments.Add(Convert(attachment)); + } + + // set body + if (!message.IsBodyHtml) + { + data.Text = message.Body; + } + else + { + data.Html = message.Body; + } + + try + { + using HttpResponseMessage responseMessage = await Http.PostAsJsonAsync(uri, data, JsonSerializerOptions, token); + LettermintResponse.SendEmail response = await responseMessage.Content.ReadFromJsonAsync(JsonSerializerOptions, token); + + if (!responseMessage.IsSuccessStatusCode) + { + throw new Exception($"Could not send message via Lettermint API. Status code: {responseMessage.StatusCode}, Message: {response.ErrorMessage}"); + } + + Logger.LogDebug("Email {id} sent via Lettermint API", response.MessageId); + } + catch (Exception ex) + { + Logger.LogError(ex, "Could not send message via Lettermint API"); + } + } + + + /// + public void Dispose() { } + + + /// + /// Convert a collection of addresses to a Lettermint email addresses + /// + protected string[] Convert(MailAddressCollection addresses) + { + return addresses + .Select(address => address.ToString()) + .ToArray(); + } + + + /// + /// Convert an attachment to a Lettermint email attachment + /// + protected LettermintRequest.EmailAttachment Convert(Attachment attachment) + { + byte[] buffer = new byte[8067]; + using MemoryStream memoryStream = new(); + int count; + while ((count = attachment.ContentStream.Read(buffer, 0, buffer.Length)) > 0) + { + memoryStream.Write(buffer, 0, count); + } + string base64String = System.Convert.ToBase64String(memoryStream.ToArray()); + + return new() + { + Filename = attachment.Name, + ContentType = attachment.ContentType.MediaType, + Content = base64String, + ContentId = attachment.ContentId + }; + } +} diff --git a/mixtape/Mails/Dispatchers/Lettermint/LettermintOptions.cs b/mixtape/Mails/Dispatchers/Lettermint/LettermintOptions.cs new file mode 100644 index 00000000..6231ba3a --- /dev/null +++ b/mixtape/Mails/Dispatchers/Lettermint/LettermintOptions.cs @@ -0,0 +1,10 @@ +namespace Mixtape.Mails.Dispatchers.Lettermint; + +public class LettermintOptions +{ + public string ApiUrl { get; set; } = "https://api.lettermint.co"; + + public string Token { get; set; } + + public string Route { get; set; } = "outgoing"; +} \ No newline at end of file diff --git a/mixtape/Mails/Dispatchers/Lettermint/LettermintRequest.cs b/mixtape/Mails/Dispatchers/Lettermint/LettermintRequest.cs new file mode 100644 index 00000000..cc672de0 --- /dev/null +++ b/mixtape/Mails/Dispatchers/Lettermint/LettermintRequest.cs @@ -0,0 +1,56 @@ +using System.Text.Json.Serialization; + +namespace Mixtape.Mails.Dispatchers.Lettermint; + +public class LettermintRequest +{ + public class SendEmail + { + public string From { get; set; } + + public string[] To { get; set; } = []; + + public string[] Cc { get; set; } = []; + + public string[] Bcc { get; set; } = []; + + public string[] ReplyTo { get; set; } + + public string Subject { get; set; } + + public string Text { get; set; } + + public string Html { get; set; } + + public string Route { get; set; } + + public string Tag { get; set; } + + public List Attachments { get; set; } = []; + + public Dictionary Metadata { get; set; } = new(); + } + + public class EmailAttachment + { + /// + /// Filename of the attachment. + /// + public string Filename { get; set; } + + /// + /// MIME type of the attachment. + /// + public string ContentType { get; set; } + + /// + /// Content of the attachment encoded in base64. + /// + public string Content { get; set; } + + /// + /// Content ID for inline attachments, referenced via cid: in HTML body + /// + public string ContentId { get; set; } + } +} \ No newline at end of file diff --git a/mixtape/Mails/Dispatchers/Lettermint/LettermintResponse.cs b/mixtape/Mails/Dispatchers/Lettermint/LettermintResponse.cs new file mode 100644 index 00000000..2bbd5eeb --- /dev/null +++ b/mixtape/Mails/Dispatchers/Lettermint/LettermintResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace Mixtape.Mails.Dispatchers.Lettermint; + +public class LettermintResponse +{ + public class SendEmail + { + public string MessageId { get; set; } + + public string Status { get; set; } + + [JsonPropertyName("message")] + public string ErrorMessage { get; set; } + } +} \ No newline at end of file diff --git a/mixtape/Mails/MailOptions.cs b/mixtape/Mails/MailOptions.cs index d4fe66b3..e7543c42 100644 --- a/mixtape/Mails/MailOptions.cs +++ b/mixtape/Mails/MailOptions.cs @@ -1,4 +1,5 @@ -using Mixtape.Mails.Dispatchers.Postmark; +using Mixtape.Mails.Dispatchers.Lettermint; +using Mixtape.Mails.Dispatchers.Postmark; using Mixtape.Mails.Dispatchers.Scaleway; namespace Mixtape.Mails; @@ -25,5 +26,7 @@ public class MailOptions public ScalewayOptions Scaleway { get; set; } + public LettermintOptions Lettermint { get; set; } + public Func BuildViewPath { get; set; } } \ No newline at end of file diff --git a/mixtape/Mails/MixtapeMailModule.cs b/mixtape/Mails/MixtapeMailModule.cs index f970e8cb..55b05fa9 100644 --- a/mixtape/Mails/MixtapeMailModule.cs +++ b/mixtape/Mails/MixtapeMailModule.cs @@ -3,6 +3,7 @@ using Mixtape.Mails.Dispatchers.Postmark; using Mixtape.Mails.Dispatchers.Scaleway; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Mixtape.Mails.Dispatchers.Lettermint; namespace Mixtape.Mails; @@ -11,6 +12,8 @@ internal class MixtapeMailModule : MixtapeModule public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) { services.AddHttpClient().RemoveAllLoggers(); + services.AddHttpClient().RemoveAllLoggers(); + services.AddScoped(); // use logger mail dispatcher as default implementation diff --git a/mixtape/mixtape.csproj b/mixtape/mixtape.csproj index 786095d9..38e63954 100644 --- a/mixtape/mixtape.csproj +++ b/mixtape/mixtape.csproj @@ -18,7 +18,7 @@ - +