From c8d0c80822f96ab3c1cf4a1c70d5ad7ffe71b6fb Mon Sep 17 00:00:00 2001 From: bernd Date: Tue, 4 Aug 2026 14:14:48 +0200 Subject: [PATCH 01/15] nuget updates and support for netstandard (just not FluentEmail.Bootstrap and FluentEmail.Razor) --- src/Directory.Build.props | 3 +- src/FluentEmail.Core/FluentEmail.Core.csproj | 4 +- .../FluentEmail.Bootstrap.csproj | 3 +- .../FluentEmail.Liquid.csproj | 25 ++----- .../FluentEmail.Razor.csproj | 14 +--- .../FluentEmail.Azure.Email.csproj | 3 +- .../FluentEmail.Graph/ClientAuthHandler.cs | 49 +++++++++++++ .../FluentEmail.Graph.csproj | 19 ++--- src/Senders/FluentEmail.Graph/GraphSender.cs | 73 +++++++++++-------- .../FluentEmail.MailKit.csproj | 22 +----- .../FluentEmail.MailPace.csproj | 3 +- .../FluentEmail.Mailgun.csproj | 2 +- .../FluentEmail.Mailtrap.csproj | 6 +- .../HttpHelpers/HttpClientHelpers.cs | 2 - .../FluentEmail.Postmark.csproj | 15 +--- .../FluentEmail.SendGrid.csproj | 3 +- .../FluentEmail.Smtp/FluentEmail.Smtp.csproj | 2 +- .../FluentEmail.Bootstrap.Tests.csproj | 25 +++---- .../FluentEmail.Core.Tests.csproj | 6 ++ .../FluentEmail.Liquid.Tests.csproj | 18 ++--- .../FluentEmail.Razor.Tests.csproj | 17 ++--- .../FluentEmail.ThirdParty.Tests.csproj | 8 +- 22 files changed, 167 insertions(+), 155 deletions(-) create mode 100644 src/Senders/FluentEmail.Graph/ClientAuthHandler.cs diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 74719804..270e904c 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 abab3a94..daf073ae 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 65bf82f6..b7e2cca8 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 c5ad7654..1878dbc1 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 16da77d0..1751f4f9 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 d13fb37c..0b3c66c1 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 00000000..1e114a8a --- /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 c40499d9..de898104 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 963ee65a..b44740a1 100644 --- a/src/Senders/FluentEmail.Graph/GraphSender.cs +++ b/src/Senders/FluentEmail.Graph/GraphSender.cs @@ -1,48 +1,56 @@ -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; 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; + private readonly bool _saveSent = saveSentItems; + private readonly GraphServiceClient _graphClient = graphClient; + + public GraphSender(IAuthenticationProvider authProvider, + bool saveSentItems, + string baseUrl = null) + : this( + new GraphServiceClient(authProvider, baseUrl ?? "https://graph.microsoft.com/v1.0"), + saveSentItems + ) + { + } - private ClientCredentialProvider _authProvider; - private GraphServiceClient _graphClient; - private IConfidentialClientApplication _clientApp; + public GraphSender( + TokenCredential tokenCredential, + bool SaveSentItems, + IEnumerable scopes = null, + string baseUrl = null + ) : this( + new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider(tokenCredential, null, null, true, scopes?.ToArray() ?? []), + SaveSentItems, + baseUrl + ) + { + } public GraphSender( string GraphEmailAppId, string GraphEmailTenantId, string GraphEmailSecret, - bool SaveSentItems) + bool SaveSentItems, + IEnumerable scopes = null, + string baseUrl = null) + : 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(); - - _authProvider = new ClientCredentialProvider(_clientApp); - - _graphClient = new GraphServiceClient(_authProvider); } public SendResponse Send(IFluentEmail email, CancellationToken? token = null) @@ -120,7 +128,7 @@ public async Task SendAsync(IFluentEmail email, CancellationToken? if(email.Data.Attachments != null && email.Data.Attachments.Count > 0) { - message.Attachments = new MessageAttachmentsCollectionPage(); + message.Attachments = []; email.Data.Attachments.ForEach(a => { @@ -153,11 +161,12 @@ public async Task SendAsync(IFluentEmail email, CancellationToken? try { - await _graphClient.Users[email.Data.FromAddress.EmailAddress] - .SendMail(message, _saveSent) - .Request() - .PostAsync(); - + var builder = _graphClient.Users[email.Data.FromAddress.EmailAddress].SendMail; + await builder.PostAsync(new() + { + Message = message, + SaveToSentItems = _saveSent + }); return new SendResponse { MessageId = message.Id diff --git a/src/Senders/FluentEmail.MailKit/FluentEmail.MailKit.csproj b/src/Senders/FluentEmail.MailKit/FluentEmail.MailKit.csproj index d46b82b8..7ed347d5 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 55746578..6951988d 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.Mailgun/FluentEmail.Mailgun.csproj b/src/Senders/FluentEmail.Mailgun/FluentEmail.Mailgun.csproj index 528c52c0..9ae4cc91 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 d3b0ef28..69521278 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 6a38db8d..6dc79a15 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 fd0a3588..7f1dbdc6 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 9bd4ac74..d133d612 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 071f0ee0..13d792aa 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 0a3b6eee..7f57c591 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 581497f4..04641f6a 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 5cd74fdb..4d42ed94 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 51fd01ad..1dd2436f 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 72a60dde..843b4bde 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 @@ + + + + + + From 35e6c4a79094f225cc41a8ddd99de88984d24fd4 Mon Sep 17 00:00:00 2001 From: bernd Date: Tue, 4 Aug 2026 14:18:14 +0200 Subject: [PATCH 02/15] ReadExactly for NETSTANDARD --- .../FluentEmail.MailPace/StreamExtensions.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/Senders/FluentEmail.MailPace/StreamExtensions.cs b/src/Senders/FluentEmail.MailPace/StreamExtensions.cs index 964af477..46319330 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 From 3725f338f536267371df3daad67ed830424a5094 Mon Sep 17 00:00:00 2001 From: bernd Date: Tue, 4 Aug 2026 14:29:55 +0200 Subject: [PATCH 03/15] cancellationToken was not used in PostAsync --- src/Senders/FluentEmail.Graph/GraphSender.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Senders/FluentEmail.Graph/GraphSender.cs b/src/Senders/FluentEmail.Graph/GraphSender.cs index b44740a1..b06da0c2 100644 --- a/src/Senders/FluentEmail.Graph/GraphSender.cs +++ b/src/Senders/FluentEmail.Graph/GraphSender.cs @@ -162,11 +162,14 @@ public async Task SendAsync(IFluentEmail email, CancellationToken? try { var builder = _graphClient.Users[email.Data.FromAddress.EmailAddress].SendMail; - await builder.PostAsync(new() - { - Message = message, - SaveToSentItems = _saveSent - }); + await builder.PostAsync( + body: new() + { + Message = message, + SaveToSentItems = _saveSent + }, + cancellationToken: token.GetValueOrDefault() + ); return new SendResponse { MessageId = message.Id From d598303a0b429c960fd763eaab5e7fc8d63ee5d4 Mon Sep 17 00:00:00 2001 From: bernd Date: Tue, 4 Aug 2026 14:31:57 +0200 Subject: [PATCH 04/15] DefaultSaveSentItems --- src/Senders/FluentEmail.Graph/GraphSender.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Senders/FluentEmail.Graph/GraphSender.cs b/src/Senders/FluentEmail.Graph/GraphSender.cs index b06da0c2..8d944168 100644 --- a/src/Senders/FluentEmail.Graph/GraphSender.cs +++ b/src/Senders/FluentEmail.Graph/GraphSender.cs @@ -14,8 +14,10 @@ namespace FluentEmail.Graph { - public class GraphSender(GraphServiceClient graphClient, bool saveSentItems) : ISender + public class GraphSender(GraphServiceClient graphClient, bool saveSentItems = GraphSender.DefaultSaveSentItems) : ISender { + public const bool DefaultSaveSentItems = true; + private readonly bool _saveSent = saveSentItems; private readonly GraphServiceClient _graphClient = graphClient; @@ -31,7 +33,7 @@ public GraphSender(IAuthenticationProvider authProvider, public GraphSender( TokenCredential tokenCredential, - bool SaveSentItems, + bool SaveSentItems = DefaultSaveSentItems, IEnumerable scopes = null, string baseUrl = null ) : this( @@ -46,7 +48,7 @@ public GraphSender( string GraphEmailAppId, string GraphEmailTenantId, string GraphEmailSecret, - bool SaveSentItems, + bool SaveSentItems = DefaultSaveSentItems, IEnumerable scopes = null, string baseUrl = null) : this(new ClientAuthHandler(GraphEmailAppId, GraphEmailTenantId, GraphEmailSecret), SaveSentItems, scopes, baseUrl) From 5691f3a377ecc01ff1b6feb8fbfc5e5acbfc6455 Mon Sep 17 00:00:00 2001 From: bernd Date: Tue, 4 Aug 2026 14:47:15 +0200 Subject: [PATCH 05/15] default is already handled in GraphServiceClient --- src/Senders/FluentEmail.Graph/GraphSender.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Senders/FluentEmail.Graph/GraphSender.cs b/src/Senders/FluentEmail.Graph/GraphSender.cs index 8d944168..5ffb73c1 100644 --- a/src/Senders/FluentEmail.Graph/GraphSender.cs +++ b/src/Senders/FluentEmail.Graph/GraphSender.cs @@ -25,7 +25,7 @@ public GraphSender(IAuthenticationProvider authProvider, bool saveSentItems, string baseUrl = null) : this( - new GraphServiceClient(authProvider, baseUrl ?? "https://graph.microsoft.com/v1.0"), + new GraphServiceClient(authProvider, baseUrl), saveSentItems ) { From 543cf7dc211601b1f6f8a217b6e2548fcaa57af9 Mon Sep 17 00:00:00 2001 From: bernd Date: Tue, 4 Aug 2026 14:50:15 +0200 Subject: [PATCH 06/15] implement cancellationToken rule without named arguments --- src/Senders/FluentEmail.Graph/GraphSender.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Senders/FluentEmail.Graph/GraphSender.cs b/src/Senders/FluentEmail.Graph/GraphSender.cs index 5ffb73c1..25589e51 100644 --- a/src/Senders/FluentEmail.Graph/GraphSender.cs +++ b/src/Senders/FluentEmail.Graph/GraphSender.cs @@ -165,12 +165,13 @@ public async Task SendAsync(IFluentEmail email, CancellationToken? { var builder = _graphClient.Users[email.Data.FromAddress.EmailAddress].SendMail; await builder.PostAsync( - body: new() + new() { Message = message, SaveToSentItems = _saveSent }, - cancellationToken: token.GetValueOrDefault() + default, + token.GetValueOrDefault() ); return new SendResponse { From 99224f3c2793a4f64d23a9ff39b2fdcef8af8c75 Mon Sep 17 00:00:00 2001 From: bernd Date: Tue, 4 Aug 2026 15:02:47 +0200 Subject: [PATCH 07/15] add support for missing from addr (fallback to user account) --- src/Senders/FluentEmail.Graph/GraphSender.cs | 82 +++++++++++++------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/src/Senders/FluentEmail.Graph/GraphSender.cs b/src/Senders/FluentEmail.Graph/GraphSender.cs index 25589e51..0331ab27 100644 --- a/src/Senders/FluentEmail.Graph/GraphSender.cs +++ b/src/Senders/FluentEmail.Graph/GraphSender.cs @@ -55,12 +55,7 @@ public GraphSender( { } - public SendResponse Send(IFluentEmail email, CancellationToken? token = null) - { - return SendAsync(email, token).GetAwaiter().GetResult(); - } - - public async Task SendAsync(IFluentEmail email, CancellationToken? token = null) + protected virtual Message CreateMessage(IFluentEmail email) { var message = new Message { @@ -80,14 +75,14 @@ public async Task SendAsync(IFluentEmail email, CancellationToken? } }; - if(email.Data.ToAddresses != null && email.Data.ToAddresses.Count > 0) + if (email.Data.ToAddresses != null && email.Data.ToAddresses.Count > 0) { var toRecipients = new List(); email.Data.ToAddresses.ForEach(r => toRecipients.Add(new Recipient { EmailAddress = new EmailAddress - { + { Address = r.EmailAddress.ToString(), Name = r.Name } @@ -96,7 +91,7 @@ public async Task SendAsync(IFluentEmail email, CancellationToken? message.ToRecipients = toRecipients; } - if(email.Data.BccAddresses != null && email.Data.BccAddresses.Count > 0) + if (email.Data.BccAddresses != null && email.Data.BccAddresses.Count > 0) { var bccRecipients = new List(); @@ -128,7 +123,7 @@ public async Task SendAsync(IFluentEmail email, CancellationToken? message.CcRecipients = ccRecipients; } - if(email.Data.Attachments != null && email.Data.Attachments.Count > 0) + if (email.Data.Attachments != null && email.Data.Attachments.Count > 0) { message.Attachments = []; @@ -145,7 +140,7 @@ public async Task SendAsync(IFluentEmail email, CancellationToken? }); } - switch(email.Data.Priority) + switch (email.Data.Priority) { case Priority.High: message.Importance = Importance.High; @@ -160,40 +155,69 @@ public async Task SendAsync(IFluentEmail email, CancellationToken? message.Importance = Importance.Normal; break; } + return message; + } + + public SendResponse Send(IFluentEmail email, CancellationToken? token = null) + { + return SendAsync(email, token).GetAwaiter().GetResult(); + } + public async Task SendAsync(IFluentEmail email, CancellationToken? token = null) + { try { - var builder = _graphClient.Users[email.Data.FromAddress.EmailAddress].SendMail; - await builder.PostAsync( - new() + var message = CreateMessage(email); + var cancellationToken = token.GetValueOrDefault(); + if (email is { Data.FromAddress.EmailAddress: { Length: > 0 } addr}) + { + var builder = _graphClient.Users[addr].SendMail; + await builder.PostAsync( + new() + { + Message = message, + SaveToSentItems = _saveSent + }, + default, + cancellationToken + ); + return new SendResponse { - Message = message, - SaveToSentItems = _saveSent - }, - default, - token.GetValueOrDefault() - ); - return new SendResponse + MessageId = message.Id + }; + } + else { - MessageId = message.Id - }; + 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(); } } } From 6f4a9e97e4341c1b4f47594d88a2b593d8602224 Mon Sep 17 00:00:00 2001 From: bernd Date: Tue, 4 Aug 2026 15:12:42 +0200 Subject: [PATCH 08/15] codacy issues --- src/Senders/FluentEmail.Graph/GraphSender.cs | 75 +++++++++++++++++--- 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/src/Senders/FluentEmail.Graph/GraphSender.cs b/src/Senders/FluentEmail.Graph/GraphSender.cs index 0331ab27..d5afeae1 100644 --- a/src/Senders/FluentEmail.Graph/GraphSender.cs +++ b/src/Senders/FluentEmail.Graph/GraphSender.cs @@ -12,18 +12,29 @@ using System.Threading; using System.Threading.Tasks; +#nullable enable + namespace FluentEmail.Graph { - public class GraphSender(GraphServiceClient graphClient, bool saveSentItems = GraphSender.DefaultSaveSentItems) : ISender + public class GraphSender(GraphServiceClient graphClient, bool saveSentItems) : ISender { - public const bool DefaultSaveSentItems = true; + public static readonly bool DefaultSaveSentItems = true; private readonly bool _saveSent = saveSentItems; private readonly GraphServiceClient _graphClient = graphClient; - public GraphSender(IAuthenticationProvider authProvider, - bool saveSentItems, - string baseUrl = null) + 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 @@ -33,9 +44,9 @@ public GraphSender(IAuthenticationProvider authProvider, public GraphSender( TokenCredential tokenCredential, - bool SaveSentItems = DefaultSaveSentItems, - IEnumerable scopes = null, - string baseUrl = null + bool SaveSentItems, + IEnumerable scopes, + string? baseUrl ) : this( new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider(tokenCredential, null, null, true, scopes?.ToArray() ?? []), SaveSentItems, @@ -44,17 +55,59 @@ public GraphSender( { } + public GraphSender( + TokenCredential tokenCredential, + bool SaveSentItems, + IEnumerable scopes + ) : this( + new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider(tokenCredential, null, null, true, scopes?.ToArray() ?? []), + SaveSentItems, + null + ) + { + } + + public GraphSender( + TokenCredential tokenCredential, + bool SaveSentItems + ) : this( + new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider(tokenCredential, null, null, true, []), + SaveSentItems, + null + ) + { + } + public GraphSender( string GraphEmailAppId, string GraphEmailTenantId, string GraphEmailSecret, - bool SaveSentItems = DefaultSaveSentItems, - IEnumerable scopes = null, - string baseUrl = null) + bool SaveSentItems, + IEnumerable scopes, + string? baseUrl) : this(new ClientAuthHandler(GraphEmailAppId, GraphEmailTenantId, GraphEmailSecret), SaveSentItems, scopes, baseUrl) { } + public GraphSender( + string GraphEmailAppId, + string GraphEmailTenantId, + string GraphEmailSecret, + bool SaveSentItems) + : this(new ClientAuthHandler(GraphEmailAppId, GraphEmailTenantId, GraphEmailSecret), SaveSentItems) + { + } + + public GraphSender( + string GraphEmailAppId, + string GraphEmailTenantId, + string GraphEmailSecret, + bool SaveSentItems, + IEnumerable scopes) + : this(new ClientAuthHandler(GraphEmailAppId, GraphEmailTenantId, GraphEmailSecret), SaveSentItems, scopes, null) + { + } + protected virtual Message CreateMessage(IFluentEmail email) { var message = new Message From 2d39d3ad6ab068ae5dc72754893cb75e1f497aef Mon Sep 17 00:00:00 2001 From: bernd Date: Tue, 4 Aug 2026 15:15:55 +0200 Subject: [PATCH 09/15] add non nullable api for cancellationToken --- src/Senders/FluentEmail.Graph/GraphSender.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Senders/FluentEmail.Graph/GraphSender.cs b/src/Senders/FluentEmail.Graph/GraphSender.cs index d5afeae1..8fdf3cb9 100644 --- a/src/Senders/FluentEmail.Graph/GraphSender.cs +++ b/src/Senders/FluentEmail.Graph/GraphSender.cs @@ -216,12 +216,16 @@ public SendResponse Send(IFluentEmail email, CancellationToken? token = null) return SendAsync(email, token).GetAwaiter().GetResult(); } - public async Task SendAsync(IFluentEmail email, CancellationToken? token = null) + public Task SendAsync(IFluentEmail email, CancellationToken? token = null) + { + return SendAsync(email, token.GetValueOrDefault()); + } + + public async Task SendAsync(IFluentEmail email, CancellationToken cancellationToken) { try { var message = CreateMessage(email); - var cancellationToken = token.GetValueOrDefault(); if (email is { Data.FromAddress.EmailAddress: { Length: > 0 } addr}) { var builder = _graphClient.Users[addr].SendMail; From 1557ea40a92e9acf2d95ccedfa01d53b4f390e70 Mon Sep 17 00:00:00 2001 From: bernd Date: Tue, 4 Aug 2026 15:23:25 +0200 Subject: [PATCH 10/15] use CancellationToken.None for missing value, reduce scope to array code --- src/Senders/FluentEmail.Graph/GraphSender.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Senders/FluentEmail.Graph/GraphSender.cs b/src/Senders/FluentEmail.Graph/GraphSender.cs index 8fdf3cb9..49150e26 100644 --- a/src/Senders/FluentEmail.Graph/GraphSender.cs +++ b/src/Senders/FluentEmail.Graph/GraphSender.cs @@ -60,8 +60,9 @@ public GraphSender( bool SaveSentItems, IEnumerable scopes ) : this( - new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider(tokenCredential, null, null, true, scopes?.ToArray() ?? []), + tokenCredential, SaveSentItems, + scopes, null ) { @@ -218,7 +219,14 @@ public SendResponse Send(IFluentEmail email, CancellationToken? token = null) public Task SendAsync(IFluentEmail email, CancellationToken? token = null) { - return SendAsync(email, token.GetValueOrDefault()); + if (token.HasValue) + { + return SendAsync(email, token.Value); + } + else + { + return SendAsync(email, CancellationToken.None); + } } public async Task SendAsync(IFluentEmail email, CancellationToken cancellationToken) From 57435c0e22bcdba154d948c964c84b315b1a71df Mon Sep 17 00:00:00 2001 From: bernd Date: Tue, 4 Aug 2026 15:27:02 +0200 Subject: [PATCH 11/15] use Array.Empty --- src/Senders/FluentEmail.Graph/GraphSender.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Senders/FluentEmail.Graph/GraphSender.cs b/src/Senders/FluentEmail.Graph/GraphSender.cs index 49150e26..70e90baf 100644 --- a/src/Senders/FluentEmail.Graph/GraphSender.cs +++ b/src/Senders/FluentEmail.Graph/GraphSender.cs @@ -72,7 +72,7 @@ public GraphSender( TokenCredential tokenCredential, bool SaveSentItems ) : this( - new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider(tokenCredential, null, null, true, []), + new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider(tokenCredential, null, null, true, Array.Empty()), SaveSentItems, null ) From 0605d2954e53fcc8121eb0ae2653b64b5654ffc0 Mon Sep 17 00:00:00 2001 From: bernd Date: Tue, 4 Aug 2026 15:32:31 +0200 Subject: [PATCH 12/15] remove default value --- src/Senders/FluentEmail.Graph/GraphSender.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Senders/FluentEmail.Graph/GraphSender.cs b/src/Senders/FluentEmail.Graph/GraphSender.cs index 70e90baf..94060262 100644 --- a/src/Senders/FluentEmail.Graph/GraphSender.cs +++ b/src/Senders/FluentEmail.Graph/GraphSender.cs @@ -217,7 +217,7 @@ public SendResponse Send(IFluentEmail email, CancellationToken? token = null) return SendAsync(email, token).GetAwaiter().GetResult(); } - public Task SendAsync(IFluentEmail email, CancellationToken? token = null) + public Task SendAsync(IFluentEmail email, CancellationToken? token) { if (token.HasValue) { @@ -229,6 +229,9 @@ public Task SendAsync(IFluentEmail email, CancellationToken? token } } + public Task SendAsync(IFluentEmail email) + => SendAsync(email, CancellationToken.None); + public async Task SendAsync(IFluentEmail email, CancellationToken cancellationToken) { try From 7ce61fd8aed70b952e9b22a8a4d3b7a86b703dd7 Mon Sep 17 00:00:00 2001 From: bernd Date: Tue, 4 Aug 2026 15:39:01 +0200 Subject: [PATCH 13/15] add explicit interface impl for: "Add the default parameter value defined in the overridden method." --- src/Senders/FluentEmail.Graph/GraphSender.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Senders/FluentEmail.Graph/GraphSender.cs b/src/Senders/FluentEmail.Graph/GraphSender.cs index 94060262..ba293f2d 100644 --- a/src/Senders/FluentEmail.Graph/GraphSender.cs +++ b/src/Senders/FluentEmail.Graph/GraphSender.cs @@ -212,6 +212,11 @@ protected virtual Message CreateMessage(IFluentEmail email) 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(); From cb3c9f1dc17601b800887de5f5bcdee8d3a4043c Mon Sep 17 00:00:00 2001 From: bernd Date: Tue, 4 Aug 2026 16:11:25 +0200 Subject: [PATCH 14/15] Recipient handling simplified and fix added for #39 --- src/Senders/FluentEmail.Graph/GraphSender.cs | 126 +++++++++---------- 1 file changed, 60 insertions(+), 66 deletions(-) diff --git a/src/Senders/FluentEmail.Graph/GraphSender.cs b/src/Senders/FluentEmail.Graph/GraphSender.cs index ba293f2d..cfdfac55 100644 --- a/src/Senders/FluentEmail.Graph/GraphSender.cs +++ b/src/Senders/FluentEmail.Graph/GraphSender.cs @@ -109,6 +109,44 @@ public GraphSender( { } + 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 + } + }; + } + + private static List? CreateRecipients(IList
recipients) + { + 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; + } + protected virtual Message CreateMessage(IFluentEmail email) { var message = new Message @@ -117,98 +155,54 @@ protected virtual Message CreateMessage(IFluentEmail email) 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 = []; - - email.Data.Attachments.ForEach(a => + foreach(var a in email.Data.Attachments) { var attachment = new FileAttachment { Name = a.Filename, ContentType = a.ContentType, + IsInline = a.IsInline, ContentBytes = GetAttachmentBytes(a.Data) }; - message.Attachments.Add(attachment); - }); + } } - switch (email.Data.Priority) + message.Importance = email.Data.Priority switch { - 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; - } + Priority.High => (Importance?)Importance.High, + Priority.Normal => (Importance?)Importance.Normal, + Priority.Low => (Importance?)Importance.Low, + _ => (Importance?)Importance.Normal, + }; return message; } From f2a54265e216fc47e849a81c1666a41a881ff617 Mon Sep 17 00:00:00 2001 From: bernd Date: Tue, 4 Aug 2026 17:38:51 +0200 Subject: [PATCH 15/15] header handling added --- src/Senders/FluentEmail.Graph/GraphSender.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Senders/FluentEmail.Graph/GraphSender.cs b/src/Senders/FluentEmail.Graph/GraphSender.cs index cfdfac55..a5bf9908 100644 --- a/src/Senders/FluentEmail.Graph/GraphSender.cs +++ b/src/Senders/FluentEmail.Graph/GraphSender.cs @@ -189,6 +189,7 @@ protected virtual Message CreateMessage(IFluentEmail email) { Name = a.Filename, ContentType = a.ContentType, + ContentId = a.ContentId, IsInline = a.IsInline, ContentBytes = GetAttachmentBytes(a.Data) }; @@ -203,6 +204,15 @@ protected virtual Message CreateMessage(IFluentEmail email) 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; + } + return message; }