diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 7471980..270e904 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - net8.0;net9.0 + net8.0;net9.0;netstandard2.0 Luke Lowrey;Ben Cull;Github Contributors;John Campion email;smtp;fluent;fluentemail https://raw.githubusercontent.com/lukencode/FluentEmail/master/assets/fluentemail_logo_64x64.png @@ -39,6 +39,7 @@ + diff --git a/src/FluentEmail.Core/FluentEmail.Core.csproj b/src/FluentEmail.Core/FluentEmail.Core.csproj index abab3a9..daf073a 100644 --- a/src/FluentEmail.Core/FluentEmail.Core.csproj +++ b/src/FluentEmail.Core/FluentEmail.Core.csproj @@ -19,12 +19,12 @@ - + - + diff --git a/src/Renderers/FluentEmail.Bootstrap/FluentEmail.Bootstrap.csproj b/src/Renderers/FluentEmail.Bootstrap/FluentEmail.Bootstrap.csproj index 65bf82f..b7e2cca 100644 --- a/src/Renderers/FluentEmail.Bootstrap/FluentEmail.Bootstrap.csproj +++ b/src/Renderers/FluentEmail.Bootstrap/FluentEmail.Bootstrap.csproj @@ -1,6 +1,7 @@  + net8.0;net9.0 Process the FluentEmail templates through UnDotNet.BootstrapEmail. Allows for simpler templates. Fluent Email - Bootstrap John Campion @@ -24,7 +25,7 @@ - + diff --git a/src/Renderers/FluentEmail.Liquid/FluentEmail.Liquid.csproj b/src/Renderers/FluentEmail.Liquid/FluentEmail.Liquid.csproj index c5ad765..1878dbc 100644 --- a/src/Renderers/FluentEmail.Liquid/FluentEmail.Liquid.csproj +++ b/src/Renderers/FluentEmail.Liquid/FluentEmail.Liquid.csproj @@ -13,21 +13,13 @@ - + + + + + + - - - - - - - - - - - - - @@ -35,9 +27,4 @@ - - - - - diff --git a/src/Renderers/FluentEmail.Razor/FluentEmail.Razor.csproj b/src/Renderers/FluentEmail.Razor/FluentEmail.Razor.csproj index 16da77d..1751f4f 100644 --- a/src/Renderers/FluentEmail.Razor/FluentEmail.Razor.csproj +++ b/src/Renderers/FluentEmail.Razor/FluentEmail.Razor.csproj @@ -1,6 +1,7 @@  + net8.0;net9.0 Generate emails using Razor templates. Anything you can do in ASP.NET is possible here. Uses the RazorLight project under the hood. Fluent Email - Razor $(PackageTags);razor @@ -10,18 +11,9 @@ jcamp.$(AssemblyName) - - - - - - - - - - + @@ -33,7 +25,7 @@ - + diff --git a/src/Senders/FluentEmail.Azure.Email/FluentEmail.Azure.Email.csproj b/src/Senders/FluentEmail.Azure.Email/FluentEmail.Azure.Email.csproj index d13fb37..0b3c66c 100644 --- a/src/Senders/FluentEmail.Azure.Email/FluentEmail.Azure.Email.csproj +++ b/src/Senders/FluentEmail.Azure.Email/FluentEmail.Azure.Email.csproj @@ -19,10 +19,11 @@ + - + diff --git a/src/Senders/FluentEmail.Graph/ClientAuthHandler.cs b/src/Senders/FluentEmail.Graph/ClientAuthHandler.cs new file mode 100644 index 0000000..1e114a8 --- /dev/null +++ b/src/Senders/FluentEmail.Graph/ClientAuthHandler.cs @@ -0,0 +1,49 @@ +using Azure.Core; +using Microsoft.Graph.Auth; +using Microsoft.Identity.Client; +using Microsoft.Kiota.Abstractions; +using Microsoft.Kiota.Abstractions.Authentication; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace FluentEmail.Graph +{ + internal class ClientAuthHandler : TokenCredential + { + private readonly IConfidentialClientApplication _clientApp; + + public ClientAuthHandler(string appId, string tenantId, string graphSecret) + { + var builder = ConfidentialClientApplicationBuilder + .Create(appId) + .WithTenantId(tenantId) + .WithClientSecret(graphSecret); + + _clientApp = builder.Build(); + } + + public override async ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + var result = await _clientApp + .AcquireTokenForClient(requestContext.Scopes) + .ExecuteAsync(cancellationToken) + .ConfigureAwait(false); + + return new AccessToken(result.AccessToken, result.ExpiresOn); + } + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + var result = _clientApp + .AcquireTokenForClient(requestContext.Scopes) + .ExecuteAsync(cancellationToken) + .GetAwaiter() + .GetResult(); + + return new AccessToken(result.AccessToken, result.ExpiresOn); + } + } +} diff --git a/src/Senders/FluentEmail.Graph/FluentEmail.Graph.csproj b/src/Senders/FluentEmail.Graph/FluentEmail.Graph.csproj index c40499d..de89810 100644 --- a/src/Senders/FluentEmail.Graph/FluentEmail.Graph.csproj +++ b/src/Senders/FluentEmail.Graph/FluentEmail.Graph.csproj @@ -10,21 +10,12 @@ - + - + - - - - - - - - - - - + + @@ -36,7 +27,7 @@ - + diff --git a/src/Senders/FluentEmail.Graph/GraphSender.cs b/src/Senders/FluentEmail.Graph/GraphSender.cs index 963ee65..a5bf990 100644 --- a/src/Senders/FluentEmail.Graph/GraphSender.cs +++ b/src/Senders/FluentEmail.Graph/GraphSender.cs @@ -1,56 +1,153 @@ -using FluentEmail.Core; +using Azure.Core; +using FluentEmail.Core; using FluentEmail.Core.Interfaces; using FluentEmail.Core.Models; using Microsoft.Graph; -using Microsoft.Graph.Auth; -using Microsoft.Identity.Client; +using Microsoft.Graph.Models; +using Microsoft.Kiota.Abstractions.Authentication; using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; +#nullable enable + namespace FluentEmail.Graph { - public class GraphSender : ISender + public class GraphSender(GraphServiceClient graphClient, bool saveSentItems) : ISender { - private readonly string _appId; - private readonly string _tenantId; - private readonly string _graphSecret; - private bool _saveSent; + public static readonly bool DefaultSaveSentItems = true; + + private readonly bool _saveSent = saveSentItems; + private readonly GraphServiceClient _graphClient = graphClient; + + public GraphSender(GraphServiceClient graphClient) + : this(graphClient, DefaultSaveSentItems) + { } + + public GraphSender( + IAuthenticationProvider authProvider, + bool saveSentItems) + : this(authProvider, saveSentItems, null) + { + } + + public GraphSender(IAuthenticationProvider authProvider, bool saveSentItems, string? baseUrl) + : this( + new GraphServiceClient(authProvider, baseUrl), + saveSentItems + ) + { + } + + public GraphSender( + TokenCredential tokenCredential, + bool SaveSentItems, + IEnumerable scopes, + string? baseUrl + ) : this( + new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider(tokenCredential, null, null, true, scopes?.ToArray() ?? []), + SaveSentItems, + baseUrl + ) + { + } - private ClientCredentialProvider _authProvider; - private GraphServiceClient _graphClient; - private IConfidentialClientApplication _clientApp; + public GraphSender( + TokenCredential tokenCredential, + bool SaveSentItems, + IEnumerable scopes + ) : this( + tokenCredential, + SaveSentItems, + scopes, + null + ) + { + } + + public GraphSender( + TokenCredential tokenCredential, + bool SaveSentItems + ) : this( + new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider(tokenCredential, null, null, true, Array.Empty()), + SaveSentItems, + null + ) + { + } public GraphSender( string GraphEmailAppId, string GraphEmailTenantId, string GraphEmailSecret, - bool SaveSentItems) + bool SaveSentItems, + IEnumerable scopes, + string? baseUrl) + : this(new ClientAuthHandler(GraphEmailAppId, GraphEmailTenantId, GraphEmailSecret), SaveSentItems, scopes, baseUrl) { - _appId = GraphEmailAppId; - _tenantId = GraphEmailTenantId; - _graphSecret = GraphEmailSecret; - _saveSent = SaveSentItems; + } - _clientApp = ConfidentialClientApplicationBuilder - .Create(_appId) - .WithTenantId(_tenantId) - .WithClientSecret(_graphSecret) - .Build(); + public GraphSender( + string GraphEmailAppId, + string GraphEmailTenantId, + string GraphEmailSecret, + bool SaveSentItems) + : this(new ClientAuthHandler(GraphEmailAppId, GraphEmailTenantId, GraphEmailSecret), SaveSentItems) + { + } - _authProvider = new ClientCredentialProvider(_clientApp); + public GraphSender( + string GraphEmailAppId, + string GraphEmailTenantId, + string GraphEmailSecret, + bool SaveSentItems, + IEnumerable scopes) + : this(new ClientAuthHandler(GraphEmailAppId, GraphEmailTenantId, GraphEmailSecret), SaveSentItems, scopes, null) + { + } - _graphClient = new GraphServiceClient(_authProvider); + private static Recipient? CreateRecipient(Address address) + { + if (address == null || string.IsNullOrWhiteSpace(address.EmailAddress)) + { + return null; + } + return new Recipient + { + EmailAddress = new EmailAddress + { + Address = address.EmailAddress, + Name = address.Name + } + }; } - public SendResponse Send(IFluentEmail email, CancellationToken? token = null) + private static List? CreateRecipients(IList
recipients) { - return SendAsync(email, token).GetAwaiter().GetResult(); + if (recipients.Count == 0) + { + return null; + } + var result = new List(); + foreach (var r in recipients) + { + var recipient = CreateRecipient(r); + if (recipient != null) + { + result.Add(recipient); + } + } + if (result.Count == 0) + { + return null; + } + return result; } - public async Task SendAsync(IFluentEmail email, CancellationToken? token = null) + protected virtual Message CreateMessage(IFluentEmail email) { var message = new Message { @@ -58,127 +155,146 @@ public async Task SendAsync(IFluentEmail email, CancellationToken? Body = new ItemBody { Content = email.Data.Body, - ContentType = email.Data.IsHtml ? BodyType.Html : BodyType.Text - }, - From = new Recipient - { - EmailAddress = new EmailAddress - { - Address = email.Data.FromAddress.EmailAddress, - Name = email.Data.FromAddress.Name - } + ContentType = email.Data.IsHtml ? BodyType.Html : BodyType.Text, } }; - if(email.Data.ToAddresses != null && email.Data.ToAddresses.Count > 0) + if (CreateRecipient(email.Data.FromAddress) is { } f) + { + message.From = f; + } + if (CreateRecipients(email.Data.ReplyToAddresses) is { } replyTos) + { + message.ReplyTo = replyTos; + } + if (CreateRecipients(email.Data.ToAddresses) is { } toRecipients) { - var toRecipients = new List(); - - email.Data.ToAddresses.ForEach(r => toRecipients.Add(new Recipient - { - EmailAddress = new EmailAddress - { - Address = r.EmailAddress.ToString(), - Name = r.Name - } - })); - message.ToRecipients = toRecipients; } - - if(email.Data.BccAddresses != null && email.Data.BccAddresses.Count > 0) + if (CreateRecipients(email.Data.BccAddresses) is { } bccRecipients) { - var bccRecipients = new List(); - - email.Data.BccAddresses.ForEach(r => bccRecipients.Add(new Recipient - { - EmailAddress = new EmailAddress - { - Address = r.EmailAddress.ToString(), - Name = r.Name - } - })); - message.BccRecipients = bccRecipients; } - - if (email.Data.CcAddresses != null && email.Data.CcAddresses.Count > 0) + if (CreateRecipients(email.Data.CcAddresses) is { } ccRecipients) { - var ccRecipients = new List(); - - email.Data.CcAddresses.ForEach(r => ccRecipients.Add(new Recipient - { - EmailAddress = new EmailAddress - { - Address = r.EmailAddress.ToString(), - Name = r.Name - } - })); - message.CcRecipients = ccRecipients; } - if(email.Data.Attachments != null && email.Data.Attachments.Count > 0) + if (email.Data.Attachments is { Count: > 0 }) { - message.Attachments = new MessageAttachmentsCollectionPage(); - - email.Data.Attachments.ForEach(a => + message.Attachments = []; + foreach(var a in email.Data.Attachments) { var attachment = new FileAttachment { Name = a.Filename, ContentType = a.ContentType, + ContentId = a.ContentId, + IsInline = a.IsInline, ContentBytes = GetAttachmentBytes(a.Data) }; - message.Attachments.Add(attachment); - }); + } } - switch(email.Data.Priority) - { - case Priority.High: - message.Importance = Importance.High; - break; - case Priority.Normal: - message.Importance = Importance.Normal; - break; - case Priority.Low: - message.Importance = Importance.Low; - break; - default: - message.Importance = Importance.Normal; - break; + message.Importance = email.Data.Priority switch + { + Priority.High => (Importance?)Importance.High, + Priority.Normal => (Importance?)Importance.Normal, + Priority.Low => (Importance?)Importance.Low, + _ => (Importance?)Importance.Normal, + }; + + if (email.Data.Headers.Any()) + { + var headers = email.Data.Headers + .Select(header => new InternetMessageHeader { Name = header.Key, Value = header.Value }) + .ToList(); + message.InternetMessageHeaders = headers; } - try + return message; + } + + Task ISender.SendAsync(IFluentEmail email, CancellationToken? token) + { + return SendAsync(email, token); + } + + public SendResponse Send(IFluentEmail email, CancellationToken? token = null) + { + return SendAsync(email, token).GetAwaiter().GetResult(); + } + + public Task SendAsync(IFluentEmail email, CancellationToken? token) + { + if (token.HasValue) + { + return SendAsync(email, token.Value); + } + else { - await _graphClient.Users[email.Data.FromAddress.EmailAddress] - .SendMail(message, _saveSent) - .Request() - .PostAsync(); + return SendAsync(email, CancellationToken.None); + } + } - return new SendResponse + public Task SendAsync(IFluentEmail email) + => SendAsync(email, CancellationToken.None); + + public async Task SendAsync(IFluentEmail email, CancellationToken cancellationToken) + { + try + { + var message = CreateMessage(email); + if (email is { Data.FromAddress.EmailAddress: { Length: > 0 } addr}) { - MessageId = message.Id - }; + var builder = _graphClient.Users[addr].SendMail; + await builder.PostAsync( + new() + { + Message = message, + SaveToSentItems = _saveSent + }, + default, + cancellationToken + ); + return new SendResponse + { + MessageId = message.Id + }; + } + else + { + var builder = _graphClient.Me.SendMail; + await builder.PostAsync( + new() + { + Message = message, + SaveToSentItems = _saveSent + }, + default, + cancellationToken + ); + return new SendResponse + { + MessageId = message.Id + }; + } } catch (Exception ex) { return new SendResponse { - ErrorMessages = new List { ex.Message } + ErrorMessages = [ ex.Message ] }; } } private static byte[] GetAttachmentBytes(Stream stream) { - using(MemoryStream m = new MemoryStream()) - { - stream.CopyTo(m); - return m.ToArray(); - } + using var m = new MemoryStream(); + stream.CopyTo(m); + return m.ToArray(); } } } diff --git a/src/Senders/FluentEmail.MailKit/FluentEmail.MailKit.csproj b/src/Senders/FluentEmail.MailKit/FluentEmail.MailKit.csproj index d46b82b..7ed347d 100644 --- a/src/Senders/FluentEmail.MailKit/FluentEmail.MailKit.csproj +++ b/src/Senders/FluentEmail.MailKit/FluentEmail.MailKit.csproj @@ -19,23 +19,9 @@ - - - - - - - - - - - - - - - - - - + + + + diff --git a/src/Senders/FluentEmail.MailPace/FluentEmail.MailPace.csproj b/src/Senders/FluentEmail.MailPace/FluentEmail.MailPace.csproj index 5574657..6951988 100644 --- a/src/Senders/FluentEmail.MailPace/FluentEmail.MailPace.csproj +++ b/src/Senders/FluentEmail.MailPace/FluentEmail.MailPace.csproj @@ -10,6 +10,7 @@ + @@ -21,7 +22,7 @@ - + \ No newline at end of file diff --git a/src/Senders/FluentEmail.MailPace/StreamExtensions.cs b/src/Senders/FluentEmail.MailPace/StreamExtensions.cs index 964af47..4631933 100644 --- a/src/Senders/FluentEmail.MailPace/StreamExtensions.cs +++ b/src/Senders/FluentEmail.MailPace/StreamExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Buffers; +using System.Diagnostics; using System.IO; namespace FluentEmail.MailPace; @@ -20,4 +22,55 @@ public static string ConvertToBase64(this Stream stream) return Convert.ToBase64String(bytes); } + +#if NETSTANDARD + private static void ReadExactly(this Stream s, byte[] buffer, int offset, int count) + { + _ = s.ReadAtLeastCore(buffer.AsSpan(offset, count), count, throwOnEndOfStream: true); + } + + private static int Read(this Stream s, Span buffer) + { + byte[] sharedBuffer = ArrayPool.Shared.Rent(buffer.Length); + try + { + int numRead = s.Read(sharedBuffer, 0, buffer.Length); + if ((uint)numRead > (uint)buffer.Length) + { + throw new IOException("IO_StreamTooLong"); + } + + new ReadOnlySpan(sharedBuffer, 0, numRead).CopyTo(buffer); + return numRead; + } + finally + { + ArrayPool.Shared.Return(sharedBuffer); + } + } + + private static int ReadAtLeastCore(this Stream s, Span buffer, int minimumBytes, bool throwOnEndOfStream) + { + Debug.Assert(minimumBytes <= buffer.Length); + + int totalRead = 0; + while (totalRead < minimumBytes) + { + int read = s.Read(buffer.Slice(totalRead)); + if (read == 0) + { + if (throwOnEndOfStream) + { + throw new EndOfStreamException(); + } + + return totalRead; + } + + totalRead += read; + } + + return totalRead; + } +#endif } \ No newline at end of file diff --git a/src/Senders/FluentEmail.Mailgun/FluentEmail.Mailgun.csproj b/src/Senders/FluentEmail.Mailgun/FluentEmail.Mailgun.csproj index 528c52c..9ae4cc9 100644 --- a/src/Senders/FluentEmail.Mailgun/FluentEmail.Mailgun.csproj +++ b/src/Senders/FluentEmail.Mailgun/FluentEmail.Mailgun.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/Senders/FluentEmail.Mailtrap/FluentEmail.Mailtrap.csproj b/src/Senders/FluentEmail.Mailtrap/FluentEmail.Mailtrap.csproj index d3b0ef2..6952127 100644 --- a/src/Senders/FluentEmail.Mailtrap/FluentEmail.Mailtrap.csproj +++ b/src/Senders/FluentEmail.Mailtrap/FluentEmail.Mailtrap.csproj @@ -18,7 +18,11 @@ - + + + + + diff --git a/src/Senders/FluentEmail.Mailtrap/HttpHelpers/HttpClientHelpers.cs b/src/Senders/FluentEmail.Mailtrap/HttpHelpers/HttpClientHelpers.cs index 6a38db8..6dc79a1 100644 --- a/src/Senders/FluentEmail.Mailtrap/HttpHelpers/HttpClientHelpers.cs +++ b/src/Senders/FluentEmail.Mailtrap/HttpHelpers/HttpClientHelpers.cs @@ -6,8 +6,6 @@ using System.Text; using System.Threading.Tasks; using System.Text.Json; -using FluentEmail.Core; -using System.Net.Http.Headers; namespace FluentEmail.Mailtrap.HttpHelpers { diff --git a/src/Senders/FluentEmail.Postmark/FluentEmail.Postmark.csproj b/src/Senders/FluentEmail.Postmark/FluentEmail.Postmark.csproj index fd0a358..7f1dbdc 100644 --- a/src/Senders/FluentEmail.Postmark/FluentEmail.Postmark.csproj +++ b/src/Senders/FluentEmail.Postmark/FluentEmail.Postmark.csproj @@ -10,25 +10,16 @@ - + + - - - - - - - - - - - + diff --git a/src/Senders/FluentEmail.SendGrid/FluentEmail.SendGrid.csproj b/src/Senders/FluentEmail.SendGrid/FluentEmail.SendGrid.csproj index 9bd4ac7..d133d61 100644 --- a/src/Senders/FluentEmail.SendGrid/FluentEmail.SendGrid.csproj +++ b/src/Senders/FluentEmail.SendGrid/FluentEmail.SendGrid.csproj @@ -18,11 +18,12 @@ + - + diff --git a/src/Senders/FluentEmail.Smtp/FluentEmail.Smtp.csproj b/src/Senders/FluentEmail.Smtp/FluentEmail.Smtp.csproj index 071f0ee..13d792a 100644 --- a/src/Senders/FluentEmail.Smtp/FluentEmail.Smtp.csproj +++ b/src/Senders/FluentEmail.Smtp/FluentEmail.Smtp.csproj @@ -18,7 +18,7 @@ - + diff --git a/test/FluentEmail.Bootstrap.Tests/FluentEmail.Bootstrap.Tests.csproj b/test/FluentEmail.Bootstrap.Tests/FluentEmail.Bootstrap.Tests.csproj index 0a3b6ee..7f57c59 100644 --- a/test/FluentEmail.Bootstrap.Tests/FluentEmail.Bootstrap.Tests.csproj +++ b/test/FluentEmail.Bootstrap.Tests/FluentEmail.Bootstrap.Tests.csproj @@ -10,21 +10,14 @@ - + + + + + + + + + - - - - - - - - - - - - - - - diff --git a/test/FluentEmail.Core.Tests/FluentEmail.Core.Tests.csproj b/test/FluentEmail.Core.Tests/FluentEmail.Core.Tests.csproj index 581497f..04641f6 100644 --- a/test/FluentEmail.Core.Tests/FluentEmail.Core.Tests.csproj +++ b/test/FluentEmail.Core.Tests/FluentEmail.Core.Tests.csproj @@ -25,4 +25,10 @@ + + + + + + diff --git a/test/FluentEmail.Liquid.Tests/FluentEmail.Liquid.Tests.csproj b/test/FluentEmail.Liquid.Tests/FluentEmail.Liquid.Tests.csproj index 5cd74fd..4d42ed9 100644 --- a/test/FluentEmail.Liquid.Tests/FluentEmail.Liquid.Tests.csproj +++ b/test/FluentEmail.Liquid.Tests/FluentEmail.Liquid.Tests.csproj @@ -17,17 +17,15 @@ - - - - - - - + + + - - - + + + + + diff --git a/test/FluentEmail.Razor.Tests/FluentEmail.Razor.Tests.csproj b/test/FluentEmail.Razor.Tests/FluentEmail.Razor.Tests.csproj index 51fd01a..1dd2436 100644 --- a/test/FluentEmail.Razor.Tests/FluentEmail.Razor.Tests.csproj +++ b/test/FluentEmail.Razor.Tests/FluentEmail.Razor.Tests.csproj @@ -17,16 +17,6 @@ - - - - - - - - - - @@ -36,4 +26,11 @@ + + + + + + + diff --git a/test/FluentEmail.ThirdParty.Tests/FluentEmail.ThirdParty.Tests.csproj b/test/FluentEmail.ThirdParty.Tests/FluentEmail.ThirdParty.Tests.csproj index 72a60dd..843b4bd 100644 --- a/test/FluentEmail.ThirdParty.Tests/FluentEmail.ThirdParty.Tests.csproj +++ b/test/FluentEmail.ThirdParty.Tests/FluentEmail.ThirdParty.Tests.csproj @@ -19,7 +19,7 @@ PreserveNewest - + PreserveNewest @@ -34,4 +34,10 @@ + + + + + +