├── test ├── FluentEmail.Core.Tests │ ├── test.txt │ ├── test-embedded.txt │ ├── test.he-IL.txt │ ├── EmailTemplates │ │ └── test-embedded.txt │ ├── logotest.png │ ├── test-binary.xlsx │ ├── GlobalUsings.cs │ ├── ReplaceRendererTest.cs │ ├── FluentEmail.Core.Tests.csproj │ ├── AttachmentTests.cs │ ├── AddressTests.cs │ └── SmtpSenderTests.cs ├── FluentEmail.ThirdParty.Tests │ ├── test.txt │ ├── test-embedded.txt │ ├── test.he-IL.txt │ ├── logotest.png │ ├── test-binary.xlsx │ ├── GlobalUsings.cs │ ├── .env.sample │ ├── FluentEmail.ThirdParty.Tests.csproj │ ├── _Credentials.cs │ ├── GraphSenderTests.cs │ ├── AzureEmailSenderTests.cs │ ├── MailtrapSenderTests.cs │ └── MailgunSenderTests.cs ├── FluentEmail.Razor.Tests │ ├── Shared │ │ └── _Layout.cshtml │ ├── _EmbeddedLayout.cshtml │ └── FluentEmail.Razor.Tests.csproj ├── FluentEmail.Liquid.Tests │ ├── EmailTemplates │ │ ├── _embedded.liquid │ │ └── _layout.liquid │ ├── ComplexModel │ │ ├── TestData.cs │ │ ├── ParentModel.cs │ │ └── ComplexModelRenderTests.cs │ └── FluentEmail.Liquid.Tests.csproj ├── FluentEmail.Bootstrap.Tests │ ├── AcceptVerifyChanges.sh │ ├── AcceptVerifyChanges.ps1 │ ├── FluentEmail.Bootstrap.Tests.csproj │ ├── BootstrapTests.CompileBootstrap_Compiles.verified.txt │ ├── BootstrapTests.UsingBootstrapBody_Compiles.verified.txt │ ├── BootstrapTests.UsingBootstrapTemplate_Compiles.verified.txt │ └── BootstrapTests.cs └── Directory.Build.props ├── .release-please-manifest.json ├── assets ├── fluentemail_logo.png └── fluentemail_logo_64x64.png ├── packages └── repositories.config ├── src ├── Senders │ ├── FluentEmail.Mailgun │ │ ├── MailGunRegion.cs │ │ ├── MailgunResponse.cs │ │ ├── HttpHelpers │ │ │ ├── HttpFile.cs │ │ │ ├── BasicAuthHeader.cs │ │ │ └── ApiResponse.cs │ │ ├── IFluentEmailExtensions.cs │ │ ├── FluentEmailMailgunBuilderExtensions.cs │ │ ├── FluentEmail.Mailgun.csproj │ │ ├── IMailgunSender.cs │ │ ├── README.md │ │ └── MailgunSender.cs │ ├── FluentEmail.SendGrid │ │ ├── Changelog.md │ │ ├── README.md │ │ ├── IFluentEmailExtensions.cs │ │ ├── FluentEmailSendGridBuilderExtensions.cs │ │ ├── FluentEmail.SendGrid.csproj │ │ └── ISendGridSender.cs │ ├── FluentEmail.Mailtrap │ │ ├── MailtrapResponse.cs │ │ ├── README.md │ │ ├── IFluentEmailExtensions.cs │ │ ├── FluentEmailMailtrapBuilderExtensions.cs │ │ ├── HttpHelpers │ │ │ ├── ApiResponse.cs │ │ │ └── HttpClientHelpers.cs │ │ ├── FluentEmail.Mailtrap.csproj │ │ ├── IMailtrapSender.cs │ │ └── MailtrapSender.cs │ ├── FluentEmail.Graph │ │ ├── README.md │ │ ├── FluentEmailGraphBuilderExtensions.cs │ │ ├── FluentEmail.Graph.csproj │ │ └── GraphSender.cs │ ├── FluentEmail.Postmark │ │ ├── README.md │ │ ├── FluentEmail.Postmark.csproj │ │ ├── PostmarkSenderOptions.cs │ │ ├── FluentEmailExtensions.cs │ │ ├── FluentEmailPostmarkBuilderExtensions.cs │ │ └── PostmarkSender.cs │ ├── FluentEmail.Azure.Email │ │ ├── README.md │ │ ├── FluentEmail.Azure.Email.csproj │ │ └── FluentEmailAzureEmailBuilderExtensions.cs │ ├── FluentEmail.MailPace │ │ ├── README.md │ │ ├── MailPaceAttachment.cs │ │ ├── MailPaceResponse.cs │ │ ├── StreamExtensions.cs │ │ ├── FluentEmailMailPaceBuilderExtensions.cs │ │ ├── FluentEmail.MailPace.csproj │ │ ├── MailPaceSendRequest.cs │ │ └── MailPaceSender.cs │ ├── FluentEmail.Smtp │ │ ├── README.md │ │ ├── FluentEmail.Smtp.csproj │ │ └── FluentEmailSmtpBuilderExtensions.cs │ ├── FluentEmail.MailKit │ │ ├── README.md │ │ ├── SmtpClientOptions.cs │ │ ├── FluentEmail.MailKit.csproj │ │ └── FluentEmailMailKitBuilderExtensions.cs │ └── FluentEmail.Exchange │ │ └── ExchangeSender.cs ├── FluentEmail.Core │ ├── IFluentEmailFactory.cs │ ├── Models │ │ ├── Priority.cs │ │ ├── Attachment.cs │ │ ├── SendResponse.cs │ │ ├── Address.cs │ │ └── EmailData.cs │ ├── Interfaces │ │ ├── ITemplateRenderer.cs │ │ └── ISender.cs │ ├── FluentEmailFactory.cs │ ├── EmbeddedResourceHelper.cs │ ├── Defaults │ │ ├── ReplaceRenderer.cs │ │ └── SaveToDiskSender.cs │ ├── ListExtensions.cs │ ├── FluentEmail.Core.csproj │ ├── FluentEmailServiceCollectionExtensions.cs │ ├── IHideObjectMembers.cs │ └── EmbeddedTemplates.cs ├── Renderers │ ├── FluentEmail.Razor │ │ ├── IViewBagModel.cs │ │ ├── README.md │ │ ├── InMemoryRazorLightProject.cs │ │ ├── FluentEmail.Razor.csproj │ │ ├── RazorRenderer.cs │ │ └── FluentEmailRazorBuilderExtensions.cs │ ├── FluentEmail.Liquid │ │ ├── README.md │ │ ├── LiquidRendererOptions.cs │ │ ├── FluentEmail.Liquid.csproj │ │ ├── LiquidParser.cs │ │ ├── FluentEmailFluidBuilderExtensions.cs │ │ └── LiquidRenderer.cs │ └── FluentEmail.Bootstrap │ │ ├── README.md │ │ ├── FluentEmail.Bootstrap.csproj │ │ └── FluentEmailFluidBuilderExtensions.cs └── Directory.Build.props ├── .github └── workflows │ ├── release-please.yml │ ├── dotnet-test.yml │ └── publish-nuget.yml ├── FluentEmail.sln.DotSettings ├── license.txt ├── .gitattributes ├── release-please-config.json ├── CHANGELOG.md └── .gitignore /test/FluentEmail.Core.Tests/test.txt: -------------------------------------------------------------------------------- 1 | yo email ##Test## -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "4.0.0" 3 | } 4 | -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/test-embedded.txt: -------------------------------------------------------------------------------- 1 | yo email ##Test## -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/test.he-IL.txt: -------------------------------------------------------------------------------- 1 | hebrew email ##Test## -------------------------------------------------------------------------------- /test/FluentEmail.ThirdParty.Tests/test.txt: -------------------------------------------------------------------------------- 1 | yo email ##Test## -------------------------------------------------------------------------------- /test/FluentEmail.ThirdParty.Tests/test-embedded.txt: -------------------------------------------------------------------------------- 1 | yo email ##Test## -------------------------------------------------------------------------------- /test/FluentEmail.ThirdParty.Tests/test.he-IL.txt: -------------------------------------------------------------------------------- 1 | hebrew email ##Test## -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/EmailTemplates/test-embedded.txt: -------------------------------------------------------------------------------- 1 | yo email ##Test## -------------------------------------------------------------------------------- /test/FluentEmail.Razor.Tests/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |

@ViewBag.Title

2 |
@RenderBody()
-------------------------------------------------------------------------------- /test/FluentEmail.Razor.Tests/_EmbeddedLayout.cshtml: -------------------------------------------------------------------------------- 1 |

@ViewBag.Title

2 |
@RenderBody()
-------------------------------------------------------------------------------- /test/FluentEmail.Liquid.Tests/EmailTemplates/_embedded.liquid: -------------------------------------------------------------------------------- 1 |

Hello!

2 |
{% renderbody %}
-------------------------------------------------------------------------------- /test/FluentEmail.Liquid.Tests/EmailTemplates/_layout.liquid: -------------------------------------------------------------------------------- 1 |

Hello!

2 |
{% renderbody %}
-------------------------------------------------------------------------------- /assets/fluentemail_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcamp-code/FluentEmail/HEAD/assets/fluentemail_logo.png -------------------------------------------------------------------------------- /assets/fluentemail_logo_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcamp-code/FluentEmail/HEAD/assets/fluentemail_logo_64x64.png -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/logotest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcamp-code/FluentEmail/HEAD/test/FluentEmail.Core.Tests/logotest.png -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/test-binary.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcamp-code/FluentEmail/HEAD/test/FluentEmail.Core.Tests/test-binary.xlsx -------------------------------------------------------------------------------- /test/FluentEmail.ThirdParty.Tests/logotest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcamp-code/FluentEmail/HEAD/test/FluentEmail.ThirdParty.Tests/logotest.png -------------------------------------------------------------------------------- /test/FluentEmail.ThirdParty.Tests/test-binary.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcamp-code/FluentEmail/HEAD/test/FluentEmail.ThirdParty.Tests/test-binary.xlsx -------------------------------------------------------------------------------- /packages/repositories.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailgun/MailGunRegion.cs: -------------------------------------------------------------------------------- 1 | namespace FluentEmail.Mailgun 2 | { 3 | public enum MailGunRegion 4 | { 5 | USA, 6 | EU 7 | } 8 | } -------------------------------------------------------------------------------- /src/FluentEmail.Core/IFluentEmailFactory.cs: -------------------------------------------------------------------------------- 1 | namespace FluentEmail.Core 2 | { 3 | public interface IFluentEmailFactory 4 | { 5 | IFluentEmail Create(); 6 | } 7 | } -------------------------------------------------------------------------------- /src/Senders/FluentEmail.SendGrid/Changelog.md: -------------------------------------------------------------------------------- 1 | ## 2.6.0 2 | - Fixed #129 - Update Sendgrid dependancy 3 | ## 2.0.2 4 | - Fixed #64 - Binary attachments weren't being encoded correctly 5 | -------------------------------------------------------------------------------- /src/Renderers/FluentEmail.Razor/IViewBagModel.cs: -------------------------------------------------------------------------------- 1 | using System.Dynamic; 2 | 3 | namespace FluentEmail.Razor 4 | { 5 | public interface IViewBagModel 6 | { 7 | ExpandoObject ViewBag { get; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/FluentEmail.Core/Models/Priority.cs: -------------------------------------------------------------------------------- 1 | namespace FluentEmail.Core.Models 2 | { 3 | public enum Priority 4 | { 5 | High = 1, 6 | Normal = 2, 7 | Low = 3 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailgun/MailgunResponse.cs: -------------------------------------------------------------------------------- 1 | namespace FluentEmail.Mailgun 2 | { 3 | public class MailgunResponse 4 | { 5 | public string Id { get; set; } 6 | public string Message { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailtrap/MailtrapResponse.cs: -------------------------------------------------------------------------------- 1 | namespace FluentEmail.Mailtrap 2 | { 3 | public class MailtrapResponse 4 | { 5 | public string Id { get; set; } 6 | public string Message { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/FluentEmail.Liquid.Tests/ComplexModel/TestData.cs: -------------------------------------------------------------------------------- 1 | namespace FluentEmail.Liquid.Tests.ComplexModel; 2 | 3 | public static class TestData 4 | { 5 | public const string ToEmail = "bob@test.com"; 6 | public const string FromEmail = "johno@test.com"; 7 | public const string Subject = "sup dawg"; 8 | } -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailtrap/README.md: -------------------------------------------------------------------------------- 1 | ![fluent email logo](https://raw.githubusercontent.com/lukencode/FluentEmail/master/assets/fluentemail_logo_64x64.png "FluentEmail") 2 | 3 | # FluentEmail - All in one email sender for .NET and .NET Core 4 | 5 | ## Mailtrap Sender for [FluentEmail](https://github.com/jcamp-code/FluentEmail) 6 | 7 | Send emails to Mailtrap. -------------------------------------------------------------------------------- /src/FluentEmail.Core/Interfaces/ITemplateRenderer.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace FluentEmail.Core.Interfaces 4 | { 5 | public interface ITemplateRenderer 6 | { 7 | string Parse(string template, T model, bool isHtml = true); 8 | Task ParseAsync(string template, T model, bool isHtml = true); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.SendGrid/README.md: -------------------------------------------------------------------------------- 1 | ![fluent email logo](https://raw.githubusercontent.com/lukencode/FluentEmail/master/assets/fluentemail_logo_64x64.png "FluentEmail") 2 | 3 | # FluentEmail - All in one email sender for .NET and .NET Core 4 | 5 | ## SendGrid Email Sender for [FluentEmail](https://github.com/jcamp-code/FluentEmail) 6 | 7 | Send email via the SendGrid API. -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Graph/README.md: -------------------------------------------------------------------------------- 1 | ![fluent email logo](https://raw.githubusercontent.com/lukencode/FluentEmail/master/assets/fluentemail_logo_64x64.png "FluentEmail") 2 | 3 | # FluentEmail - All in one email sender for .NET and .NET Core 4 | 5 | ## Microsoft Graph Sender for [FluentEmail](https://github.com/jcamp-code/FluentEmail) 6 | 7 | Send emails via Microsoft Graph's APIs -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Postmark/README.md: -------------------------------------------------------------------------------- 1 | ![fluent email logo](https://raw.githubusercontent.com/lukencode/FluentEmail/master/assets/fluentemail_logo_64x64.png "FluentEmail") 2 | 3 | # FluentEmail - All in one email sender for .NET and .NET Core 4 | 5 | ## Postmark Email Sender for [FluentEmail](https://github.com/jcamp-code/FluentEmail) 6 | 7 | Send emails via Postmark's REST API. -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailgun/HttpHelpers/HttpFile.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace FluentEmail.Mailgun.HttpHelpers 4 | { 5 | public class HttpFile 6 | { 7 | public string ParameterName { get; set; } 8 | public string Filename { get; set; } 9 | public Stream Data { get; set; } 10 | public string ContentType { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Azure.Email/README.md: -------------------------------------------------------------------------------- 1 | ![fluent email logo](https://raw.githubusercontent.com/lukencode/FluentEmail/master/assets/fluentemail_logo_64x64.png "FluentEmail") 2 | 3 | # FluentEmail - All in one email sender for .NET and .NET Core 4 | 5 | ## Azure Email Sender for [FluentEmail](https://github.com/jcamp-code/FluentEmail) 6 | 7 | Send emails via Azure Email Communication Services API -------------------------------------------------------------------------------- /src/Senders/FluentEmail.MailPace/README.md: -------------------------------------------------------------------------------- 1 | ![fluent email logo](https://raw.githubusercontent.com/lukencode/FluentEmail/master/assets/fluentemail_logo_64x64.png "FluentEmail") 2 | 3 | # FluentEmail - All in one email sender for .NET and .NET Core 4 | 5 | ## MailPace Sender for [FluentEmail](https://github.com/jcamp-code/FluentEmail) 6 | 7 | Send emails via the [MailPace](https://www.mailpace.com/) REST API. -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Smtp/README.md: -------------------------------------------------------------------------------- 1 | ![fluent email logo](https://raw.githubusercontent.com/lukencode/FluentEmail/master/assets/fluentemail_logo_64x64.png "FluentEmail") 2 | 3 | # FluentEmail - All in one email sender for .NET and .NET Core 4 | 5 | ## SMTP Email Sender for [FluentEmail](https://github.com/jcamp-code/FluentEmail) 6 | 7 | Send email via DotNet's built-in `SmtpClient`. Not recommended for production any more. -------------------------------------------------------------------------------- /src/FluentEmail.Core/Interfaces/ISender.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using FluentEmail.Core.Models; 4 | 5 | namespace FluentEmail.Core.Interfaces 6 | { 7 | public interface ISender 8 | { 9 | SendResponse Send(IFluentEmail email, CancellationToken? token = null); 10 | Task SendAsync(IFluentEmail email, CancellationToken? token = null); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | workflow_dispatch: 6 | 7 | permissions: 8 | contents: write 9 | issues: write 10 | pull-requests: write 11 | 12 | name: release-please 13 | 14 | jobs: 15 | release-please: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: googleapis/release-please-action@v4 19 | with: 20 | token: ${{ secrets.JCAMP_GH_TOKEN }} 21 | -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Global using directives 2 | 3 | global using System; 4 | global using System.IO; 5 | global using System.Linq; 6 | global using System.Threading.Tasks; 7 | global using AwesomeAssertions; 8 | global using FluentEmail.Core.Defaults; 9 | global using FluentEmail.Core.Interfaces; 10 | global using FluentEmail.Core.Models; 11 | global using Xunit; 12 | global using Attachment = FluentEmail.Core.Models.Attachment; -------------------------------------------------------------------------------- /test/FluentEmail.ThirdParty.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Global using directives 2 | 3 | global using System; 4 | global using System.IO; 5 | global using System.Linq; 6 | global using System.Threading.Tasks; 7 | global using AwesomeAssertions; 8 | global using FluentEmail.Core.Defaults; 9 | global using FluentEmail.Core.Interfaces; 10 | global using FluentEmail.Core.Models; 11 | global using Xunit; 12 | global using Attachment = FluentEmail.Core.Models.Attachment; -------------------------------------------------------------------------------- /src/FluentEmail.Core/FluentEmailFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using System; 3 | 4 | namespace FluentEmail.Core 5 | { 6 | public class FluentEmailFactory : IFluentEmailFactory 7 | { 8 | private IServiceProvider services; 9 | 10 | public FluentEmailFactory(IServiceProvider services) => this.services = services; 11 | 12 | public IFluentEmail Create() => services.GetService(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.MailPace/MailPaceAttachment.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace FluentEmail.MailPace; 4 | 5 | public class MailPaceAttachment 6 | { 7 | [JsonProperty("name")] public string Name { get; set; } 8 | [JsonProperty("content")] public string Content { get; set; } 9 | [JsonProperty("content_type")] public string ContentType { get; set; } 10 | [JsonProperty("cid", NullValueHandling = NullValueHandling.Ignore)] public string Cid { get; set; } 11 | } -------------------------------------------------------------------------------- /src/Renderers/FluentEmail.Liquid/README.md: -------------------------------------------------------------------------------- 1 | ![fluent email logo](https://raw.githubusercontent.com/lukencode/FluentEmail/master/assets/fluentemail_logo_64x64.png "FluentEmail") 2 | 3 | # FluentEmail - All in one email sender for .NET and .NET Core 4 | 5 | ## Liquid Renderer for [FluentEmail](https://github.com/jcamp-code/FluentEmail) 6 | 7 | Generate emails using [Liquid templates](https://shopify.github.io/liquid/). Uses the [Fluid](https://github.com/sebastienros/fluid) project under the hood. 8 | -------------------------------------------------------------------------------- /test/FluentEmail.Liquid.Tests/ComplexModel/ParentModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace FluentEmail.Liquid.Tests.ComplexModel; 4 | 5 | public class ParentModel 6 | { 7 | public string Id { get; set; } 8 | public NameDetails ParentName { get; set; } 9 | public List ChildrenNames { get; set; } = []; 10 | } 11 | 12 | public class NameDetails 13 | { 14 | public string Firstname { get; set; } 15 | public string Surname { get; set; } 16 | } -------------------------------------------------------------------------------- /src/Senders/FluentEmail.MailKit/README.md: -------------------------------------------------------------------------------- 1 | ![fluent email logo](https://raw.githubusercontent.com/lukencode/FluentEmail/master/assets/fluentemail_logo_64x64.png "FluentEmail") 2 | 3 | # FluentEmail - All in one email sender for .NET and .NET Core 4 | 5 | ## MailKit Email Sender for [FluentEmail](https://github.com/jcamp-code/FluentEmail) 6 | 7 | Send emails using the [MailKit](https://github.com/jstedfast/MailKit) email library. 8 | 9 | This is the recommended replacement for DotNet's built-in `SmtpClient` -------------------------------------------------------------------------------- /src/Renderers/FluentEmail.Razor/README.md: -------------------------------------------------------------------------------- 1 | ![fluent email logo](https://raw.githubusercontent.com/lukencode/FluentEmail/master/assets/fluentemail_logo_64x64.png "FluentEmail") 2 | 3 | # FluentEmail - All in one email sender for .NET and .NET Core 4 | 5 | ## Razor Renderer for [FluentEmail](https://github.com/jcamp-code/FluentEmail) 6 | 7 | Generate emails using Razor templates. Anything you can do in ASP.NET is possible here. Uses the [RazorLight](https://github.com/toddams/RazorLight) project under the hood. 8 | -------------------------------------------------------------------------------- /src/Renderers/FluentEmail.Bootstrap/README.md: -------------------------------------------------------------------------------- 1 | ![fluent email logo](https://raw.githubusercontent.com/lukencode/FluentEmail/master/assets/fluentemail_logo_64x64.png "FluentEmail") 2 | 3 | # FluentEmail - All in one email sender for .NET and .NET Core 4 | 5 | ## Bootstrap Compiler for [FluentEmail](https://github.com/jcamp-code/FluentEmail) 6 | 7 | Processes email templates after rendering through [UnDotNet.BootstrapEmail](https://github.com/UnDotNet/BootstrapEmail) to allow simpler templates to generate perfect emails. 8 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailgun/HttpHelpers/BasicAuthHeader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http.Headers; 3 | 4 | namespace FluentEmail.Mailgun.HttpHelpers 5 | { 6 | public static class BasicAuthHeader 7 | { 8 | public static AuthenticationHeaderValue GetHeader(string username, string password) 9 | { 10 | return new AuthenticationHeaderValue("Basic", Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}"))); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/FluentEmail.Bootstrap.Tests/AcceptVerifyChanges.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | ## This script is related to approval tests that protect developers from unintentional changes to the public API 3 | ## If your change does change the API on purpose and you double-checked correctness of the changes you can use this script to change the "approved" state of the API 4 | ## Make sure that you run the approval tests before running this script, because the tests generate *.received.txt files. 5 | 6 | find ./ -type f -name "*received.txt" | perl -pe 'print $_; s/received/verified/' | xargs -n2 mv 7 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.MailPace/MailPaceResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | 4 | namespace FluentEmail.MailPace; 5 | 6 | public class MailPaceResponse 7 | { 8 | [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] public string Id { get; set; } 9 | [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] public string Status { get; set; } 10 | [JsonProperty("error")] public string Error { get; set; } 11 | [JsonProperty("errors")] public Dictionary> Errors { get; set; } = new(); 12 | } -------------------------------------------------------------------------------- /FluentEmail.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | True 3 | True 4 | True -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailgun/IFluentEmailExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using FluentEmail.Core; 3 | using FluentEmail.Core.Models; 4 | 5 | namespace FluentEmail.Mailgun 6 | { 7 | public static class IFluentEmailExtensions 8 | { 9 | public static async Task SendWithTemplateAsync(this IFluentEmail email, string templateName, object templateData) 10 | { 11 | var mailgunSender = email.Sender as IMailgunSender; 12 | return await mailgunSender.SendWithTemplateAsync(email, templateName, templateData); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/FluentEmail.Core/Models/Attachment.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace FluentEmail.Core.Models 4 | { 5 | public class Attachment 6 | { 7 | /// 8 | /// Gets or sets whether the attachment is intended to be used for inline images (changes the parameter name for providers such as MailGun) 9 | /// 10 | public bool IsInline { get; set; } 11 | public string Filename { get; set; } 12 | public Stream Data { get; set; } 13 | public string ContentType { get; set; } 14 | public string ContentId { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/FluentEmail.Core/Models/SendResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace FluentEmail.Core.Models 5 | { 6 | public class SendResponse 7 | { 8 | public string MessageId { get; set; } 9 | public IList ErrorMessages { get; set; } 10 | public bool Successful => !ErrorMessages.Any(); 11 | 12 | public SendResponse() 13 | { 14 | ErrorMessages = new List(); 15 | } 16 | } 17 | 18 | public class SendResponse : SendResponse 19 | { 20 | public T Data { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailtrap/IFluentEmailExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using FluentEmail.Core; 3 | using FluentEmail.Core.Models; 4 | using FluentEmail.Mailtrap; 5 | 6 | namespace FluentEmail.Mailtrap 7 | { 8 | public static class IFluentEmailExtensions 9 | { 10 | public static async Task SendWithTemplateAsync(this IFluentEmail email, string templateName, object templateData) 11 | { 12 | var mailtrapSender = email.Sender as IMailtrapSender; 13 | return await mailtrapSender.SendWithTemplateAsync(email, templateName, templateData); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/ReplaceRendererTest.cs: -------------------------------------------------------------------------------- 1 | namespace FluentEmail.Core.Tests; 2 | 3 | public class ReplaceRendererTest 4 | { 5 | [Fact] 6 | public void ModelPropertyValueIsNull_Test() 7 | { 8 | ITemplateRenderer templateRenderer = new ReplaceRenderer(); 9 | 10 | var address = new Address("james@test.com", "james"); 11 | address.Name.Should().Be("james"); 12 | var template = "this is name: ##Name##"; 13 | templateRenderer.Parse(template, address).Should().Be("this is name: james"); 14 | 15 | address.Name = null; 16 | templateRenderer.Parse(template, address).Should().Be("this is name: "); 17 | } 18 | } -------------------------------------------------------------------------------- /src/Senders/FluentEmail.SendGrid/IFluentEmailExtensions.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core; 2 | using FluentEmail.Core.Models; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace FluentEmail.SendGrid 9 | { 10 | public static class IFluentEmailExtensions 11 | { 12 | public static async Task SendWithTemplateAsync(this IFluentEmail email, string templateId, object templateData) 13 | { 14 | var sendGridSender = email.Sender as ISendGridSender; 15 | return await sendGridSender.SendWithTemplateAsync(email, templateId, templateData); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.SendGrid/FluentEmailSendGridBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core.Interfaces; 2 | using FluentEmail.SendGrid; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | 5 | namespace Microsoft.Extensions.DependencyInjection 6 | { 7 | public static class FluentEmailSendGridBuilderExtensions 8 | { 9 | public static FluentEmailServicesBuilder AddSendGridSender(this FluentEmailServicesBuilder builder, string apiKey, bool sandBoxMode = false) 10 | { 11 | builder.Services.TryAdd(ServiceDescriptor.Singleton(_ => new SendGridSender(apiKey, sandBoxMode))); 12 | return builder; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.MailPace/StreamExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace FluentEmail.MailPace; 5 | 6 | public static class StreamExtensions 7 | { 8 | public static string ConvertToBase64(this Stream stream) 9 | { 10 | if (stream is MemoryStream memoryStream) 11 | { 12 | return Convert.ToBase64String(memoryStream.ToArray()); 13 | } 14 | 15 | var bytes = new Byte[(int)stream.Length]; 16 | 17 | stream.Seek(0, SeekOrigin.Begin); 18 | // ReSharper disable once MustUseReturnValue 19 | stream.ReadExactly(bytes, 0, (int)stream.Length); 20 | 21 | return Convert.ToBase64String(bytes); 22 | } 23 | } -------------------------------------------------------------------------------- /src/FluentEmail.Core/EmbeddedResourceHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | 5 | namespace FluentEmail.Core 6 | { 7 | internal static class EmbeddedResourceHelper 8 | { 9 | internal static string GetResourceAsString(Assembly assembly, string path) 10 | { 11 | using var stream = assembly.GetManifestResourceStream(path); 12 | if (stream is null) 13 | { 14 | throw new Exception($"{path} was not found in embedded resources."); 15 | } 16 | 17 | using var reader = new StreamReader(stream); 18 | var result = reader.ReadToEnd(); 19 | return result; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailtrap/FluentEmailMailtrapBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core.Interfaces; 2 | using FluentEmail.Mailtrap; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | 5 | namespace Microsoft.Extensions.DependencyInjection 6 | { 7 | public static class FluentEmailMailtrapBuilderExtensions 8 | { 9 | public static FluentEmailServicesBuilder AddMailtrapSender(this FluentEmailServicesBuilder builder, string userName, string password, string host = null, int? port = null) 10 | { 11 | builder.Services.TryAdd(ServiceDescriptor.Scoped(_ => new MailtrapSender(userName, password, host, port))); 12 | return builder; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.MailPace/FluentEmailMailPaceBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core.Interfaces; 2 | using FluentEmail.MailPace; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace Microsoft.Extensions.DependencyInjection 7 | { 8 | public static class FluentEmailMailPaceBuilderExtensions 9 | { 10 | public static FluentEmailServicesBuilder AddMailPaceSender( 11 | this FluentEmailServicesBuilder builder, 12 | string serverToken) 13 | { 14 | builder.Services.TryAdd(ServiceDescriptor.Scoped(_ => new MailPaceSender(serverToken))); 15 | return builder; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailgun/FluentEmailMailgunBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core.Interfaces; 2 | using FluentEmail.Mailgun; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | 5 | namespace Microsoft.Extensions.DependencyInjection 6 | { 7 | public static class FluentEmailMailgunBuilderExtensions 8 | { 9 | public static FluentEmailServicesBuilder AddMailGunSender(this FluentEmailServicesBuilder builder, string domainName, string apiKey, MailGunRegion mailGunRegion = MailGunRegion.USA) 10 | { 11 | builder.Services.TryAdd(ServiceDescriptor.Singleton(_ => new MailgunSender(domainName, apiKey, mailGunRegion))); 12 | return builder; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Renderers/FluentEmail.Razor/InMemoryRazorLightProject.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using RazorLight.Razor; 4 | 5 | namespace FluentEmail.Razor 6 | { 7 | public class InMemoryRazorLightProject : RazorLightProject 8 | { 9 | public override Task GetItemAsync(string templateKey) 10 | { 11 | return Task.FromResult(new TextSourceRazorProjectItem(templateKey, templateKey)); 12 | } 13 | 14 | public override Task> GetImportsAsync(string templateKey) 15 | { 16 | return Task.FromResult>(new List()); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailgun/HttpHelpers/ApiResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace FluentEmail.Mailgun.HttpHelpers 5 | { 6 | public class ApiResponse 7 | { 8 | public bool Success => !Errors.Any(); 9 | public IList Errors { get; set; } 10 | 11 | public ApiResponse() 12 | { 13 | Errors = new List(); 14 | } 15 | } 16 | 17 | public class ApiResponse : ApiResponse 18 | { 19 | public T Data { get; set; } 20 | } 21 | 22 | public class ApiError 23 | { 24 | public string ErrorCode { get; set; } 25 | public string ErrorMessage { get; set; } 26 | public string PropertyName { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailtrap/HttpHelpers/ApiResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace FluentEmail.Mailtrap.HttpHelpers 5 | { 6 | public class ApiResponse 7 | { 8 | public bool Success => !Errors.Any(); 9 | public IList Errors { get; set; } 10 | 11 | public ApiResponse() 12 | { 13 | Errors = new List(); 14 | } 15 | } 16 | 17 | public class ApiResponse : ApiResponse 18 | { 19 | public T Data { get; set; } 20 | } 21 | 22 | public class ApiError 23 | { 24 | public string ErrorCode { get; set; } 25 | public string ErrorMessage { get; set; } 26 | public string PropertyName { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-test.yml: -------------------------------------------------------------------------------- 1 | name: .NET Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest] 15 | 16 | steps: 17 | - name: Checkout 🛎️ 18 | uses: actions/checkout@v5 19 | 20 | - name: Setup DotNet ✨ 21 | uses: actions/setup-dotnet@v5 22 | with: 23 | dotnet-version: | 24 | 8.0.x 25 | 9.0.x 26 | 10.0.x 27 | 28 | - name: Install dependencies 📦️ 29 | run: dotnet restore 30 | 31 | - name: Build 🔨 32 | run: dotnet build --no-restore -c Release 33 | 34 | - name: Test ✅ 35 | run: dotnet test --verbosity normal 36 | -------------------------------------------------------------------------------- /test/FluentEmail.Bootstrap.Tests/AcceptVerifyChanges.ps1: -------------------------------------------------------------------------------- 1 | ## This script is related to approval tests that protect developers from unintentional changes to the public API 2 | ## If your change does change the API on purpose and you double-checked correctness of the changes you can use this script to change the "approved" state of the API 3 | ## Make sure that you run the approval tests before running this script, because the tests generate *.received.txt files. 4 | 5 | $ApprovalFiles = ".\"; 6 | 7 | ## Copy new API from .received.txt files to .verified.txt files 8 | ## Note that .received.txt files are ignored in git and are not part of the repository 9 | Get-ChildItem -Path $ApprovalFiles -Filter "*received.txt" | ForEach-Object { 10 | $NewName = $_.FullName -replace 'received.txt', 'verified.txt' 11 | Move-Item $_.FullName $NewName -Force 12 | } 13 | -------------------------------------------------------------------------------- /src/FluentEmail.Core/Defaults/ReplaceRenderer.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Threading.Tasks; 3 | using FluentEmail.Core.Interfaces; 4 | 5 | namespace FluentEmail.Core.Defaults 6 | { 7 | public class ReplaceRenderer : ITemplateRenderer 8 | { 9 | public string Parse(string template, T model, bool isHtml = true) 10 | { 11 | foreach (PropertyInfo pi in model.GetType().GetRuntimeProperties()) 12 | { 13 | template = template.Replace($"##{pi.Name}##", pi.GetValue(model, null)?.ToString()); 14 | } 15 | 16 | return template; 17 | } 18 | 19 | public Task ParseAsync(string template, T model, bool isHtml = true) 20 | { 21 | return Task.FromResult(Parse(template, model, isHtml)); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Graph/FluentEmailGraphBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core.Interfaces; 2 | using FluentEmail.Graph; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | 5 | namespace Microsoft.Extensions.DependencyInjection 6 | { 7 | public static class FluentEmailGraphBuilderExtensions 8 | { 9 | public static FluentEmailServicesBuilder AddGraphSender( 10 | this FluentEmailServicesBuilder builder, 11 | string GraphEmailAppId, 12 | string GraphEmailTenantId, 13 | string GraphEmailSecret, 14 | bool saveSentItems = false) 15 | { 16 | builder.Services.TryAdd(ServiceDescriptor.Scoped(_ => new GraphSender(GraphEmailAppId, GraphEmailTenantId, GraphEmailSecret, saveSentItems))); 17 | return builder; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/FluentEmail.Core/ListExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace FluentEmail.Core 5 | { 6 | public static class ListExtensions 7 | { 8 | public static void ForEach(this IEnumerable enumerable, Action consumer) 9 | { 10 | foreach (T item in enumerable) 11 | { 12 | consumer(item); 13 | } 14 | } 15 | 16 | public static void AddRange(this IList list, IEnumerable items) 17 | { 18 | if (list is List listT) 19 | { 20 | listT.AddRange(items); 21 | } 22 | else 23 | { 24 | foreach (T item in items) 25 | { 26 | list.Add(item); 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Smtp/FluentEmail.Smtp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Now we're talking. Send emails via SMTP. 5 | Fluent Email - SMTP 6 | Luke Lowrey;Ben Cull;Github Contributors 7 | $(PackageTags);smtp 8 | jcamp.$(AssemblyName) 9 | jcamp.$(AssemblyName) 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | true 6 | net8.0;net9.0;net10.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | all 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailtrap/FluentEmail.Mailtrap.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Send emails to Mailtrap. Uses FluentEmail.Smtp for delivery. 5 | Fluent Email - MailTrap 6 | Luke Lowrey;Ben Cull;Anthony Zigenbine;Github Contributors 7 | $(PackageTags);mailtrap 8 | jcamp.$(AssemblyName) 9 | jcamp.$(AssemblyName) 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailgun/FluentEmail.Mailgun.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Send emails via MailGun using their REST API 5 | Fluent Email - MailGun 6 | $(PackageTags);mailgun 7 | jcamp.$(AssemblyName) 8 | jcamp.$(AssemblyName) 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.MailPace/FluentEmail.MailPace.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Send emails via MailPace using their REST API 5 | Fluent Email - MailPace 6 | $(PackageTags);mailpace 7 | jcamp.$(AssemblyName) 8 | jcamp.$(AssemblyName) 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.SendGrid/FluentEmail.SendGrid.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Send emails via SendGrid using their REST API 5 | Fluent Email - SendGrid 6 | Luke Lowrey;Ben Cull;Ricardo Santos;Github Contributors 7 | $(PackageTags);sendgrid 8 | jcamp.$(AssemblyName) 9 | jcamp.$(AssemblyName) 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailgun/IMailgunSender.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using FluentEmail.Core; 4 | using FluentEmail.Core.Interfaces; 5 | using FluentEmail.Core.Models; 6 | 7 | namespace FluentEmail.Mailgun; 8 | 9 | public interface IMailgunSender : ISender 10 | { 11 | /// 12 | /// Mailgun specific extension method that allows you to use a template instead of a message body. 13 | /// For more information, see: https://documentation.mailgun.com/en/latest/api-sending.html#sending. 14 | /// 15 | /// Fluent email. 16 | /// Mailgun template name. 17 | /// Mailgun template data. 18 | /// Optional cancellation token. 19 | /// A SendResponse object. 20 | Task SendWithTemplateAsync(IFluentEmail email, string templateName, object templateData, CancellationToken? token = null); 21 | } -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailtrap/IMailtrapSender.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using FluentEmail.Core; 4 | using FluentEmail.Core.Interfaces; 5 | using FluentEmail.Core.Models; 6 | 7 | namespace FluentEmail.Mailtrap; 8 | 9 | public interface IMailtrapSender : ISender 10 | { 11 | /// 12 | /// Mailgun specific extension method that allows you to use a template instead of a message body. 13 | /// For more information, see: https://documentation.mailgun.com/en/latest/api-sending.html#sending. 14 | /// 15 | /// Fluent email. 16 | /// Mailgun template name. 17 | /// Mailgun template data. 18 | /// Optional cancellation token. 19 | /// A SendResponse object. 20 | Task SendWithTemplateAsync(IFluentEmail email, string templateName, object templateData, CancellationToken? token = null); 21 | } -------------------------------------------------------------------------------- /test/FluentEmail.ThirdParty.Tests/.env.sample: -------------------------------------------------------------------------------- 1 | FE_TEST_TO_EMAIL= 2 | FE_TEST_FROM_EMAIL= 3 | 4 | # MailTrap Tests Credentials 5 | FE_TEST_MAILTRAP_TO_EMAIL= 6 | FE_TEST_MAILTRAP_FROM_EMAIL= 7 | FE_TEST_MAILTRAP_HOST= 8 | FE_TEST_MAILTRAP_USER= 9 | FE_TEST_MAILTRAP_PWD= 10 | FE_TEST_MAILTRAP_PORT=587 11 | FE_TEST_MAILTRAP_API_HOST= 12 | FE_TEST_MAILTRAP_API_KEY= 13 | FE_TEST_MAILTRAP_TEMPLATE= 14 | 15 | # MailGun Tests Credentials 16 | FE_TEST_MAILGUN_TO_EMAIL= 17 | FE_TEST_MAILGUN_FROM_EMAIL= 18 | FE_TEST_MAILGUN_DOMAIN= 19 | FE_TEST_MAILGUN_API_KEY= 20 | 21 | # Azure Email Service Credentials 22 | FE_TEST_AZURE_TO_EMAIL= 23 | FE_TEST_AZURE_FROM_EMAIL= 24 | FE_TEST_AZURE_API_HOST= 25 | 26 | # Postmark Credentials 27 | FE_TEST_POSTMARK_API_KEY= 28 | FE_TEST_POSTMARK_FROM_EMAIL= 29 | 30 | # SendGrid Credentials 31 | FE_TEST_SENDGRID_API_KEY= 32 | FE_TEST_SENDGRID_TEMPLATE= 33 | 34 | # Graph Credentials 35 | FE_TEST_GRAPH_TO_EMAIL= 36 | FE_TEST_GRAPH_FROM_EMAIL= 37 | FE_TEST_GRAPH_TENANT_ID= 38 | FE_TEST_GRAPH_APP_ID= 39 | FE_TEST_GRAPH_CLIENT_SECRET= 40 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Azure.Email/FluentEmail.Azure.Email.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Send emails via Azure Email Communication Services API 5 | Fluent Email - Azure Email 6 | $(PackageTags);azureemail 7 | $(Version) 8 | jcamp.$(AssemblyName) 9 | jcamp.$(AssemblyName) 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/FluentEmail.Core.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | 20 | 21 | PreserveNewest 22 | 23 | 24 | PreserveNewest 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.MailKit/SmtpClientOptions.cs: -------------------------------------------------------------------------------- 1 | using MailKit.Security; 2 | using System.Net.Security; 3 | 4 | namespace FluentEmail.MailKitSmtp 5 | { 6 | public class SmtpClientOptions 7 | { 8 | public string Server { get; set; } = string.Empty; 9 | public int Port { get; set; } = 25; 10 | public string User { get; set; } = string.Empty; 11 | public string Password { get; set; } = string.Empty; 12 | public bool UseSsl { get; set; } = false; 13 | public bool RequiresAuthentication { get; set; } = false; 14 | public string PreferredEncoding { get; set; } = string.Empty; 15 | public bool UsePickupDirectory { get; set; } = false; 16 | public string MailPickupDirectory { get; set; } = string.Empty; 17 | public SecureSocketOptions? SocketOptions { get; set; } 18 | public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } 19 | 20 | /// 21 | public bool CheckCertificateRevocation { get; set; } = true; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.MailPace/MailPaceSendRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | 4 | namespace FluentEmail.MailPace; 5 | 6 | public class MailPaceSendRequest 7 | { 8 | [JsonProperty("from")] public string From { get; set; } 9 | [JsonProperty("to")] public string To { get; set; } 10 | [JsonProperty("cc", NullValueHandling = NullValueHandling.Ignore)] public string Cc { get; set; } 11 | [JsonProperty("bcc", NullValueHandling = NullValueHandling.Ignore)] public string Bcc { get; set; } 12 | [JsonProperty("subject")] public string Subject { get; set; } 13 | [JsonProperty("htmlbody", NullValueHandling = NullValueHandling.Ignore)] public string HtmlBody { get; set; } 14 | [JsonProperty("textbody", NullValueHandling = NullValueHandling.Ignore)] public string TextBody { get; set; } 15 | [JsonProperty("replyto", NullValueHandling = NullValueHandling.Ignore)] public string ReplyTo { get; set; } 16 | [JsonProperty("attachments")] public List Attachments { get; set; } = new(0); 17 | [JsonProperty("tags")] public List Tags { get; set; } = new(0); 18 | } -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Luke Lowrey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/Renderers/FluentEmail.Bootstrap/FluentEmail.Bootstrap.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Process the FluentEmail templates through UnDotNet.BootstrapEmail. Allows for simpler templates. 5 | Fluent Email - Bootstrap 6 | John Campion 7 | $(PackageTags);bootstrap 8 | ../../../assets 9 | NU5104 10 | enable 11 | jcamp.$(AssemblyName) 12 | jcamp.$(AssemblyName) 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.doc diff=astextplain 2 | *.DOC diff=astextplain 3 | *.docx diff=astextplain 4 | *.DOCX diff=astextplain 5 | *.dot diff=astextplain 6 | *.DOT diff=astextplain 7 | *.pdf diff=astextplain 8 | *.PDF diff=astextplain 9 | *.rtf diff=astextplain 10 | *.RTF diff=astextplain 11 | 12 | *.jpg binary 13 | *.png binary 14 | *.gif binary 15 | 16 | *.cs text=auto diff=csharp 17 | *.vb text=auto 18 | *.c text=auto 19 | *.cpp text=auto 20 | *.cxx text=auto 21 | *.h text=auto 22 | *.hxx text=auto 23 | *.py text=auto 24 | *.rb text=auto 25 | *.java text=auto 26 | *.html text=auto 27 | *.htm text=auto 28 | *.css text=auto 29 | *.scss text=auto 30 | *.sass text=auto 31 | *.less text=auto 32 | *.js text=auto 33 | *.lisp text=auto 34 | *.clj text=auto 35 | *.sql text=auto 36 | *.php text=auto 37 | *.lua text=auto 38 | *.m text=auto 39 | *.asm text=auto 40 | *.erl text=auto 41 | *.fs text=auto 42 | *.fsx text=auto 43 | *.hs text=auto 44 | 45 | *.csproj text=auto merge=union 46 | *.vbproj text=auto merge=union 47 | *.fsproj text=auto merge=union 48 | *.dbproj text=auto merge=union 49 | *.sln text=auto eol=crlf merge=union 50 | -------------------------------------------------------------------------------- /src/FluentEmail.Core/Models/Address.cs: -------------------------------------------------------------------------------- 1 | namespace FluentEmail.Core.Models 2 | { 3 | public class Address 4 | { 5 | public string Name { get; set; } 6 | public string EmailAddress { get; set; } 7 | 8 | public Address() 9 | { 10 | } 11 | 12 | public Address(string emailAddress, string name = null) 13 | { 14 | EmailAddress = emailAddress; 15 | Name = name; 16 | } 17 | 18 | public override string ToString() 19 | { 20 | return Name == null ? EmailAddress : $"{Name} <{EmailAddress}>"; 21 | } 22 | 23 | public override int GetHashCode() 24 | { 25 | return base.GetHashCode(); 26 | } 27 | 28 | public override bool Equals(object obj) 29 | { 30 | if ((obj == null) || !this.GetType().Equals(obj.GetType())) 31 | { 32 | return false; 33 | } 34 | else 35 | { 36 | Address otherAddress = (Address)obj; 37 | return this.EmailAddress == otherAddress.EmailAddress && this.Name == otherAddress.Name; 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.SendGrid/ISendGridSender.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core; 2 | using FluentEmail.Core.Interfaces; 3 | using FluentEmail.Core.Models; 4 | using SendGrid; 5 | using SendGrid.Helpers.Mail; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace FluentEmail.SendGrid 13 | { 14 | public interface ISendGridSender : ISender 15 | { 16 | /// 17 | /// SendGrid specific extension method that allows you to use a template instead of a message body. 18 | /// For more information, see: https://sendgrid.com/docs/ui/sending-email/how-to-send-an-email-with-dynamic-transactional-templates/. 19 | /// 20 | /// Fluent email. 21 | /// SendGrid template ID. 22 | /// SendGrid template data. 23 | /// Optional cancellation token. 24 | /// A SendResponse object. 25 | Task SendWithTemplateAsync(IFluentEmail email, string templateId, object templateData, CancellationToken? token = null); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailgun/README.md: -------------------------------------------------------------------------------- 1 | ![fluent email logo](https://raw.githubusercontent.com/lukencode/FluentEmail/master/assets/fluentemail_logo_64x64.png "FluentEmail") 2 | 3 | # FluentEmail - All in one email sender for .NET and .NET Core 4 | 5 | ## Mailgun Email Sender for [FluentEmail](https://github.com/jcamp-code/FluentEmail) 6 | 7 | Send email via the MailGun REST API. 8 | 9 | ## Usage 10 | 11 | Create a new instance of your sender and add it as the default Fluent Email sender. 12 | 13 | var sender = new MailgunSender( 14 | "sandboxcf5f41bbf2f84f15a386c60e253b5fe9.mailgun.org", // Mailgun Domain 15 | "key-8d32c046d7f14ada8d5ba8253e3e30de" // Mailgun API Key 16 | ); 17 | Email.DefaultSender = sender; 18 | 19 | /* 20 | You can optionally set the sender per instance like so: 21 | 22 | email.Sender = new MailgunSender(...); 23 | */ 24 | 25 | Send the email in the usual Fluent Email way. 26 | 27 | var email = Email 28 | .From(fromEmail) 29 | .To(toEmail) 30 | .Subject(subject) 31 | .Body(body); 32 | 33 | var response = await email.SendAsync(); 34 | 35 | 36 | ## Further Information 37 | 38 | Don't forget you can use Razor templating using the [FluentEmail.Razor](../../Renderers/FluentEmail.Razor) package as well. 39 | -------------------------------------------------------------------------------- /src/FluentEmail.Core/FluentEmail.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Send emails very easily. Use razor templates, smtp, embedded files, all without hassle. This is a Base Package and includes just the domain model, very basic defaults, and is also included with every other jcamp.FluentEmail.* package here. 5 | Fluent Email 6 | ../../assets 7 | jcamp.$(AssemblyName) 8 | jcamp.$(AssemblyName) 9 | README.md 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": { 4 | "changelog-path": "CHANGELOG.md", 5 | "release-type": "simple", 6 | "draft": false, 7 | "prerelease": false 8 | } 9 | }, 10 | "plugins": ["sentence-case"], 11 | "changelog-sections": [ 12 | { "type": "feat", "section": "✨ Features" }, 13 | { "type": "feature", "section": "✨ Features" }, 14 | { "type": "fix", "section": "🐛 Bug Fixes" }, 15 | { "type": "perf", "section": "⚡️Performance Improvements" }, 16 | { "type": "revert", "section": "⏪️ Reverts" }, 17 | { "type": "docs", "section": "📝 Documentation" }, 18 | { "type": "style", "section": "🎨 Styles" }, 19 | { "type": "chore", "section": "🏡 Miscellaneous Chores" }, 20 | { "type": "refactor", "section": "♻️ Code Refactoring", "hidden": true }, 21 | { "type": "test", "section": "✅ Tests", "hidden": true }, 22 | { "type": "build", "section": "📦️ Build System", "hidden": true }, 23 | { "type": "ci", "section": "🤖 Continuous Integration", "hidden": true } 24 | ], 25 | "extra-files": [ 26 | { 27 | "type": "xml", 28 | "path": "src/Directory.Build.props", 29 | "xpath": "//project/propertygroup/version" 30 | } 31 | ], 32 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 33 | } 34 | -------------------------------------------------------------------------------- /src/FluentEmail.Core/Models/EmailData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace FluentEmail.Core.Models 4 | { 5 | public class EmailData 6 | { 7 | public IList
ToAddresses { get; set; } 8 | public IList
CcAddresses { get; set; } 9 | public IList
BccAddresses { get; set; } 10 | public IList
ReplyToAddresses { get; set; } 11 | public IList Attachments { get; set; } 12 | public Address FromAddress { get; set; } 13 | public string Subject { get; set; } 14 | public string Body { get; set; } 15 | public string PlaintextAlternativeBody { get; set; } 16 | public Priority Priority { get; set; } 17 | public IList Tags { get; set; } 18 | 19 | public bool IsHtml { get; set; } 20 | public IDictionary Headers { get; set; } 21 | 22 | public EmailData() 23 | { 24 | ToAddresses = new List
(); 25 | CcAddresses = new List
(); 26 | BccAddresses = new List
(); 27 | ReplyToAddresses = new List
(); 28 | Attachments = new List(); 29 | Tags = new List(); 30 | Headers = new Dictionary(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/FluentEmail.Core/FluentEmailServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentEmail.Core; 3 | using FluentEmail.Core.Interfaces; 4 | using Microsoft.Extensions.DependencyInjection.Extensions; 5 | 6 | namespace Microsoft.Extensions.DependencyInjection 7 | { 8 | public static class FluentEmailServiceCollectionExtensions 9 | { 10 | public static FluentEmailServicesBuilder AddFluentEmail(this IServiceCollection services, string defaultFromEmail, string defaultFromName = "") 11 | { 12 | if (services == null) throw new ArgumentNullException(nameof(services)); 13 | 14 | var builder = new FluentEmailServicesBuilder(services); 15 | services.TryAdd(ServiceDescriptor.Transient(x => 16 | new Email(x.GetService(), x.GetService(), defaultFromEmail, defaultFromName) 17 | )); 18 | 19 | services.TryAddTransient(); 20 | 21 | return builder; 22 | } 23 | } 24 | 25 | public class FluentEmailServicesBuilder 26 | { 27 | public IServiceCollection Services { get; private set; } 28 | 29 | internal FluentEmailServicesBuilder(IServiceCollection services) 30 | { 31 | Services = services; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Postmark/FluentEmail.Postmark.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Send emails via Postmark using their REST API 5 | Fluent Email - Postmark 6 | $(PackageTags);postmark 7 | jcamp.$(AssemblyName) 8 | jcamp.$(AssemblyName) 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/FluentEmail.Bootstrap.Tests/FluentEmail.Bootstrap.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/FluentEmail.Razor.Tests/FluentEmail.Razor.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Always 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/Renderers/FluentEmail.Liquid/LiquidRendererOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Encodings.Web; 3 | using Fluid; 4 | using Microsoft.Extensions.FileProviders; 5 | 6 | namespace FluentEmail.Liquid 7 | { 8 | public class LiquidRendererOptions 9 | { 10 | /// 11 | /// Allows configuring template context before running the template. Parameters are context that has been 12 | /// prepared and the model that is going to be used. 13 | /// 14 | /// 15 | /// This API creates dependency on Fluid. 16 | /// 17 | public Action? ConfigureTemplateContext { get; set; } 18 | 19 | /// 20 | /// Text encoder to use. Defaults to . 21 | /// 22 | public TextEncoder TextEncoder { get; set; } = HtmlEncoder.Default; 23 | 24 | /// 25 | /// File provider to use, used when resolving references in templates, like master layout. 26 | /// 27 | public IFileProvider? FileProvider { get; set; } 28 | 29 | /// 30 | /// Set custom Template Options for Fluid 31 | /// 32 | public TemplateOptions TemplateOptions { get; set; } = new TemplateOptions(); 33 | 34 | /// 35 | /// Allows configuring parser 36 | /// 37 | public Action? ConfigureParser { get; set; } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/FluentEmail.Liquid.Tests/FluentEmail.Liquid.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Smtp/FluentEmailSmtpBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core.Interfaces; 2 | using FluentEmail.Smtp; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | using System; 5 | using System.Net; 6 | using System.Net.Mail; 7 | 8 | namespace Microsoft.Extensions.DependencyInjection 9 | { 10 | public static class FluentEmailSmtpBuilderExtensions 11 | { 12 | public static FluentEmailServicesBuilder AddSmtpSender(this FluentEmailServicesBuilder builder, SmtpClient smtpClient) 13 | { 14 | builder.Services.TryAdd(ServiceDescriptor.Singleton(_ => new SmtpSender(smtpClient))); 15 | return builder; 16 | } 17 | 18 | public static FluentEmailServicesBuilder AddSmtpSender(this FluentEmailServicesBuilder builder, string host, int port) => AddSmtpSender(builder, () => new SmtpClient(host, port)); 19 | 20 | public static FluentEmailServicesBuilder AddSmtpSender(this FluentEmailServicesBuilder builder, string host, int port, string username, string password) => AddSmtpSender(builder, 21 | () => new SmtpClient(host, port) { EnableSsl = true, Credentials = new NetworkCredential (username, password) }); 22 | 23 | public static FluentEmailServicesBuilder AddSmtpSender(this FluentEmailServicesBuilder builder, Func clientFactory) 24 | { 25 | builder.Services.TryAdd(ServiceDescriptor.Singleton(_ => new SmtpSender(clientFactory))); 26 | return builder; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Renderers/FluentEmail.Razor/FluentEmail.Razor.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generate emails using Razor templates. Anything you can do in ASP.NET is possible here. Uses the RazorLight project under the hood. 5 | Fluent Email - Razor 6 | $(PackageTags);razor 7 | ../../../assets 8 | NU5104 9 | jcamp.$(AssemblyName) 10 | jcamp.$(AssemblyName) 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /.github/workflows/publish-nuget.yml: -------------------------------------------------------------------------------- 1 | # Note this will only work with Release-Please if you have given it a PAT. 2 | # The default GITHUB_TOKEN does not have permission to run workflows on releases 3 | name: Publish to NuGet 4 | 5 | on: 6 | release: 7 | types: [published] 8 | workflow_dispatch: 9 | 10 | permissions: 11 | id-token: write # Required for OIDC 12 | contents: read 13 | 14 | jobs: 15 | release: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 🛎️ 19 | uses: actions/checkout@v5 20 | 21 | - name: Setup DotNet ✨ 22 | uses: actions/setup-dotnet@v5 23 | with: 24 | dotnet-version: | 25 | 8.0.x 26 | 9.0.x 27 | 10.0.x 28 | 29 | - name: Install dependencies 📦️ 30 | run: dotnet restore 31 | 32 | - name: Build 🔨 33 | run: dotnet build --no-restore -c Release 34 | 35 | # If not using true 36 | - name: Pack NuGet 🏗️ 37 | run: | 38 | dotnet pack --no-build -v normal --include-symbols -o nupkg 39 | 40 | # Get a short-lived NuGet API key 41 | - name: NuGet login (OIDC → temp API key) 42 | uses: NuGet/login@v1 43 | id: login 44 | with: 45 | user: ${{secrets.NUGET_USER}} 46 | 47 | - name: Push NuGet 🚀 48 | run: | 49 | dotnet nuget push **/*.nupkg -k ${{steps.login.outputs.NUGET_API_KEY}} -s https://api.nuget.org/v3/index.json --skip-duplicate 50 | # dotnet nuget push **/*.nupkg -k ${{secrets.NUGET_API_KEY}} -s https://f.feedz.io/jcamp/nuget-test/nuget/index.json --skip-duplicate 51 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Graph/FluentEmail.Graph.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Send emails via Microsoft Graph API 5 | Fluent Email - Graph 6 | $(PackageTags);graph 7 | NU5104 8 | jcamp.$(AssemblyName) 9 | jcamp.$(AssemblyName) 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /test/FluentEmail.ThirdParty.Tests/FluentEmail.ThirdParty.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | PreserveNewest 20 | 21 | 22 | 23 | 24 | PreserveNewest 25 | 26 | 27 | PreserveNewest 28 | 29 | 30 | PreserveNewest 31 | 32 | 33 | PreserveNewest 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.0 5 | Luke Lowrey;Ben Cull;Github Contributors;John Campion 6 | email;smtp;fluent;fluentemail 7 | https://raw.githubusercontent.com/lukencode/FluentEmail/master/assets/fluentemail_logo_64x64.png 8 | fluentemail_logo_64x64.png 9 | https://github.com/jcamp-code/FluentEmail 10 | https://github.com/jcamp-code/FluentEmail 11 | true 12 | README.md 13 | MIT 14 | 4.0.0 15 | 16 | 17 | true 18 | true 19 | true 20 | true 21 | snupkg 22 | true 23 | 24 | true 25 | true 26 | 27 | true 28 | false 29 | 30 | ../../../assets 31 | 32 | latest 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/AttachmentTests.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace FluentEmail.Core.Tests; 3 | 4 | public class AttachmentTests 5 | { 6 | private const string ToEmail = "bob@test.com"; 7 | private const string FromEmail = "johno@test.com"; 8 | private const string Subject = "sup dawg"; 9 | 10 | [Fact] 11 | public void Attachment_from_stream_Is_set() 12 | { 13 | using var stream = File.OpenRead($"{Path.Combine(Directory.GetCurrentDirectory(), "test.txt")}"); 14 | var attachment = new Attachment 15 | { 16 | Data = stream, 17 | Filename = "Test.txt", 18 | ContentType = "text/plain" 19 | }; 20 | 21 | var email = Email.From(FromEmail) 22 | .To(ToEmail) 23 | .Subject(Subject) 24 | .Attach(attachment); 25 | 26 | email.Data.Attachments.First().Data.Length.Should().Be(20); 27 | } 28 | 29 | [Fact] 30 | public void Attachment_from_filename_Is_set() 31 | { 32 | var email = Email.From(FromEmail) 33 | .To(ToEmail) 34 | .Subject(Subject) 35 | .AttachFromFilename($"{Path.Combine(Directory.GetCurrentDirectory(), "test.txt")}", "text/plain"); 36 | 37 | email.Data.Attachments.First().Data.Length.Should().Be(20); 38 | } 39 | 40 | [Fact] 41 | public void Attachment_from_filename_AttachmentName_Is_set() 42 | { 43 | var attachmentName = "attachment.txt"; 44 | var email = Email.From(FromEmail) 45 | .To(ToEmail) 46 | .Subject(Subject) 47 | .AttachFromFilename($"{Path.Combine(Directory.GetCurrentDirectory(), "test.txt")}", "text/plain", attachmentName); 48 | 49 | email.Data.Attachments.First().Data.Length.Should().Be(20); 50 | email.Data.Attachments.First().Filename.Should().Be(attachmentName); 51 | } 52 | } -------------------------------------------------------------------------------- /src/Renderers/FluentEmail.Liquid/FluentEmail.Liquid.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generate emails using Liquid templates. Uses the Fluid project under the hood. 5 | Fluent Email - Liquid 6 | Marko Lahma;$(Authors) 7 | $(PackageTags);liquid 8 | ../../../assets 9 | NU5104 10 | enable 11 | jcamp.$(AssemblyName) 12 | jcamp.$(AssemblyName) 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/Renderers/FluentEmail.Razor/RazorRenderer.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core.Interfaces; 2 | using RazorLight; 3 | using RazorLight.Razor; 4 | using System; 5 | using System.IO; 6 | using System.Security.Cryptography; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace FluentEmail.Razor 11 | { 12 | public class RazorRenderer : ITemplateRenderer 13 | { 14 | private readonly RazorLightEngine _engine; 15 | 16 | public RazorRenderer(string root = null) 17 | { 18 | _engine = new RazorLightEngineBuilder() 19 | .UseFileSystemProject(root ?? Directory.GetCurrentDirectory()) 20 | .UseMemoryCachingProvider() 21 | .Build(); 22 | } 23 | 24 | public RazorRenderer(RazorLightProject project) 25 | { 26 | _engine = new RazorLightEngineBuilder() 27 | .UseProject(project) 28 | .UseMemoryCachingProvider() 29 | .Build(); 30 | } 31 | 32 | public RazorRenderer(Type embeddedResRootType) 33 | { 34 | _engine = new RazorLightEngineBuilder() 35 | .UseEmbeddedResourcesProject(embeddedResRootType) 36 | .UseMemoryCachingProvider() 37 | .Build(); 38 | } 39 | 40 | public Task ParseAsync(string template, T model, bool isHtml = true) 41 | { 42 | dynamic viewBag = (model as IViewBagModel)?.ViewBag; 43 | return _engine.CompileRenderStringAsync(GetHashString(template), template, model, viewBag); 44 | } 45 | 46 | string ITemplateRenderer.Parse(string template, T model, bool isHtml) 47 | { 48 | return ParseAsync(template, model, isHtml).GetAwaiter().GetResult(); 49 | } 50 | 51 | public static string GetHashString(string inputString) 52 | { 53 | var sb = new StringBuilder(); 54 | var hashbytes = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(inputString)); 55 | foreach (byte b in hashbytes) 56 | { 57 | sb.Append(b.ToString("X2")); 58 | } 59 | 60 | return sb.ToString(); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.MailKit/FluentEmail.MailKit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Send emails via MailKit. The SmtpClient has been deprecated and Microsoft recommends using MailKit instead. 5 | Fluent Email - MailKit 6 | Luke Lowrey;Ben Cull;Matt Rutledge;Github Contributors 7 | $(PackageTags);mailkit 8 | FluentEmail.MailKitSmtp 9 | jcamp.$(AssemblyName) 10 | jcamp.$(AssemblyName) 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/FluentEmail.Core/IHideObjectMembers.cs: -------------------------------------------------------------------------------- 1 | // This software is part of the Autofac IoC container 2 | // Copyright (c) 2007 - 2008 Autofac Contributors 3 | // http://autofac.org 4 | // 5 | // Permission is hereby granted, free of charge, to any person 6 | // obtaining a copy of this software and associated documentation 7 | // files (the "Software"), to deal in the Software without 8 | // restriction, including without limitation the rights to use, 9 | // copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the 11 | // Software is furnished to do so, subject to the following 12 | // conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | // OTHER DEALINGS IN THE SOFTWARE 25 | 26 | using System; 27 | using System.ComponentModel; 28 | 29 | namespace FluentEmail.Core 30 | { 31 | [EditorBrowsable(EditorBrowsableState.Never)] 32 | public interface IHideObjectMembers 33 | { 34 | [EditorBrowsable(EditorBrowsableState.Never)] 35 | Type GetType(); 36 | 37 | [EditorBrowsable(EditorBrowsableState.Never)] 38 | int GetHashCode(); 39 | 40 | [EditorBrowsable(EditorBrowsableState.Never)] 41 | string ToString(); 42 | 43 | [EditorBrowsable(EditorBrowsableState.Never)] 44 | bool Equals(object obj); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Azure.Email/FluentEmailAzureEmailBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Azure; 3 | using Azure.Communication.Email; 4 | using Azure.Core; 5 | using FluentEmail.Core.Interfaces; 6 | using FluentEmail.Azure.Email; 7 | using Microsoft.Extensions.DependencyInjection.Extensions; 8 | 9 | namespace Microsoft.Extensions.DependencyInjection 10 | { 11 | public static class FluentEmailAzureEmailBuilderExtensions 12 | { 13 | public static FluentEmailServicesBuilder AddAzureEmailSender( 14 | this FluentEmailServicesBuilder builder, 15 | string connectionString) 16 | { 17 | builder.Services.TryAdd(ServiceDescriptor.Singleton((IServiceProvider x) => (ISender)(object)new AzureEmailSender(connectionString))); 18 | return builder; 19 | } 20 | 21 | public static FluentEmailServicesBuilder AddAzureEmailSender( 22 | this FluentEmailServicesBuilder builder, 23 | string connectionString, 24 | EmailClientOptions options) 25 | { 26 | builder.Services.TryAdd(ServiceDescriptor.Singleton((IServiceProvider x) => (ISender)(object)new AzureEmailSender(connectionString, options))); 27 | return builder; 28 | } 29 | 30 | public static FluentEmailServicesBuilder AddAzureEmailSender( 31 | this FluentEmailServicesBuilder builder, 32 | Uri endpoint, 33 | AzureKeyCredential keyCredential, 34 | EmailClientOptions options = default) 35 | { 36 | builder.Services.TryAdd(ServiceDescriptor.Singleton((IServiceProvider x) => (ISender)(object)new AzureEmailSender(endpoint, keyCredential, options))); 37 | return builder; 38 | } 39 | 40 | public static FluentEmailServicesBuilder AddAzureEmailSender( 41 | this FluentEmailServicesBuilder builder, 42 | Uri endpoint, 43 | TokenCredential tokenCredential, 44 | EmailClientOptions options = default) 45 | { 46 | builder.Services.TryAdd(ServiceDescriptor.Singleton((IServiceProvider x) => (ISender)(object)new AzureEmailSender(endpoint, tokenCredential, options))); 47 | return builder; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.MailKit/FluentEmailMailKitBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core.Interfaces; 2 | using FluentEmail.MailKitSmtp; 3 | using MailKit; 4 | using MailKit.Security; 5 | using Microsoft.Extensions.DependencyInjection.Extensions; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace Microsoft.Extensions.DependencyInjection 9 | { 10 | public static class FluentEmailMailKitBuilderExtensions 11 | { 12 | public static FluentEmailServicesBuilder AddMailKitSender(this FluentEmailServicesBuilder builder, SmtpClientOptions smtpClientOptions) 13 | { 14 | builder.Services.TryAdd(ServiceDescriptor.Scoped(_ => new MailKitSender(smtpClientOptions))); 15 | return builder; 16 | } 17 | public static FluentEmailServicesBuilder AddMailKitSender(this FluentEmailServicesBuilder builder, SmtpClientOptions smtpClientOptions, IProtocolLogger protocolLogger) 18 | { 19 | builder.Services.TryAdd(ServiceDescriptor.Scoped(_ => new MailKitSender(smtpClientOptions, protocolLogger))); 20 | return builder; 21 | } 22 | public static FluentEmailServicesBuilder AddMailKitSender(this FluentEmailServicesBuilder builder, string configPath = "SmtpClientOptions") 23 | { 24 | builder.Services.AddOptions().BindConfiguration(configPath); 25 | builder.Services.TryAddScoped(resolver => 26 | { 27 | var smtpOptions = resolver.GetRequiredService>(); 28 | if (smtpOptions.Value is null) 29 | { 30 | throw new System.Exception("SmtpClientOptions is null"); 31 | } 32 | if (smtpOptions.Value.User is not null && smtpOptions.Value.Password is not null) 33 | { 34 | smtpOptions.Value.RequiresAuthentication = true; 35 | smtpOptions.Value.UseSsl = true; 36 | if (smtpOptions.Value.SocketOptions is null) 37 | smtpOptions.Value.SocketOptions = SecureSocketOptions.StartTls; 38 | } 39 | return smtpOptions.Value; 40 | }); 41 | builder.Services.TryAddScoped(); 42 | return builder; 43 | } 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Renderers/FluentEmail.Razor/FluentEmailRazorBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentEmail.Core.Interfaces; 3 | using FluentEmail.Razor; 4 | using Microsoft.Extensions.DependencyInjection.Extensions; 5 | using RazorLight.Razor; 6 | 7 | namespace Microsoft.Extensions.DependencyInjection 8 | { 9 | public static class FluentEmailRazorBuilderExtensions 10 | { 11 | public static FluentEmailServicesBuilder AddRazorRenderer(this FluentEmailServicesBuilder builder) 12 | { 13 | builder.Services.TryAdd(ServiceDescriptor.Singleton(_ => new RazorRenderer())); 14 | return builder; 15 | } 16 | 17 | /// 18 | /// Add razor renderer with project views and layouts 19 | /// 20 | /// 21 | /// 22 | /// 23 | public static FluentEmailServicesBuilder AddRazorRenderer(this FluentEmailServicesBuilder builder, string templateRootFolder) 24 | { 25 | builder.Services.TryAdd(ServiceDescriptor.Singleton(_ => new RazorRenderer(templateRootFolder))); 26 | return builder; 27 | } 28 | 29 | /// 30 | /// Add razor renderer with embedded views and layouts 31 | /// 32 | /// 33 | /// 34 | /// 35 | public static FluentEmailServicesBuilder AddRazorRenderer(this FluentEmailServicesBuilder builder, Type embeddedResourceRootType) 36 | { 37 | builder.Services.TryAdd(ServiceDescriptor.Singleton(_ => new RazorRenderer( embeddedResourceRootType))); 38 | return builder; 39 | } 40 | 41 | /// 42 | /// Add razor renderer with a RazorLightProject to support views and layouts 43 | /// 44 | /// 45 | /// 46 | /// 47 | public static FluentEmailServicesBuilder AddRazorRenderer(this FluentEmailServicesBuilder builder, RazorLightProject razorLightProject) 48 | { 49 | builder.Services.TryAdd(ServiceDescriptor.Singleton(_ => new RazorRenderer(razorLightProject))); 50 | return builder; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/FluentEmail.Core/Defaults/SaveToDiskSender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using FluentEmail.Core.Interfaces; 7 | using FluentEmail.Core.Models; 8 | 9 | namespace FluentEmail.Core.Defaults 10 | { 11 | public class SaveToDiskSender : ISender 12 | { 13 | private readonly string _directory; 14 | 15 | public SaveToDiskSender(string directory) 16 | { 17 | _directory = directory; 18 | } 19 | 20 | public SendResponse Send(IFluentEmail email, CancellationToken? token = null) 21 | { 22 | return SendAsync(email, token).GetAwaiter().GetResult(); 23 | } 24 | 25 | public async Task SendAsync(IFluentEmail email, CancellationToken? token = null) 26 | { 27 | var response = new SendResponse(); 28 | await SaveEmailToDisk(email); 29 | return response; 30 | } 31 | 32 | private async Task SaveEmailToDisk(IFluentEmail email) 33 | { 34 | var random = new Random(); 35 | var filename = Path.Combine(_directory, $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{random.Next(1000)}"); 36 | 37 | using (var sw = new StreamWriter(File.OpenWrite(filename))) 38 | { 39 | await sw.WriteLineAsync($"From: {email.Data.FromAddress.Name} <{email.Data.FromAddress.EmailAddress}>"); 40 | await sw.WriteLineAsync($"To: {string.Join(",", email.Data.ToAddresses.Select(x => $"{x.Name} <{x.EmailAddress}>"))}"); 41 | await sw.WriteLineAsync($"Cc: {string.Join(",", email.Data.CcAddresses.Select(x => $"{x.Name} <{x.EmailAddress}>"))}"); 42 | await sw.WriteLineAsync($"Bcc: {string.Join(",", email.Data.BccAddresses.Select(x => $"{x.Name} <{x.EmailAddress}>"))}"); 43 | await sw.WriteLineAsync($"ReplyTo: {string.Join(",", email.Data.ReplyToAddresses.Select(x => $"{x.Name} <{x.EmailAddress}>"))}"); 44 | await sw.WriteLineAsync($"Subject: {email.Data.Subject}"); 45 | foreach (var dataHeader in email.Data.Headers) 46 | { 47 | await sw.WriteLineAsync($"{dataHeader.Key}:{dataHeader.Value}"); 48 | } 49 | await sw.WriteLineAsync(); 50 | await sw.WriteAsync(email.Data.Body); 51 | } 52 | 53 | return true; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Postmark/PostmarkSenderOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace FluentEmail.Postmark 6 | { 7 | /// 8 | /// This class provides options for a PostmarkSender instance. It allows to configure per message properties 9 | /// that aren't present in FluentEmail such as Tag. They are applied to all messages sent by the PostmarkSender 10 | /// instance that uses this options. 11 | /// 12 | public class PostmarkSenderOptions 13 | { 14 | /// 15 | /// Creates a new instance of the PostmarkSenderOptions class. 16 | /// 17 | public PostmarkSenderOptions() 18 | { 19 | } 20 | 21 | /// 22 | /// Creates a new instance of the PostmarkSenderOptions class. 23 | /// 24 | /// The serverToken to use to authenticate at the Postmark API. 25 | public PostmarkSenderOptions(string serverToken) 26 | { 27 | ServerToken = serverToken ?? throw new ArgumentNullException(nameof(serverToken)); 28 | } 29 | 30 | /// 31 | /// Sending requires server level privileges. This token can be found on the API Tokens tab under your Postmark server. 32 | /// 33 | public string ServerToken { get; set; } = ""; 34 | 35 | /// 36 | /// Activate open tracking for this email. 37 | /// 38 | public bool? TrackOpens { get; set; } 39 | 40 | /// 41 | /// Activate link tracking for links in the HTML or Text bodies of this email. Possible options: None, HtmlAndText, HtmlOnly, TextOnly 42 | /// 43 | public PostmarkDotNet.LinkTrackingOptions TrackLinks { get; set; } 44 | 45 | 46 | /// 47 | /// Custom metadata key/value pairs. 48 | /// 49 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Sammlungseigenschaften müssen schreibgeschützt sein", 50 | Justification = "This should be set-able as part of this options type.")] 51 | public Dictionary Metadata { get; set; } 52 | 53 | /// 54 | /// Email tag that allows you to categorize outgoing emails and get detailed statistics. 55 | /// 56 | public string Tag { get; set; } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/FluentEmail.Liquid.Tests/ComplexModel/ComplexModelRenderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using AwesomeAssertions; 4 | using FluentEmail.Core; 5 | using FluentEmail.Core.Interfaces; 6 | using Fluid; 7 | using Microsoft.Extensions.FileProviders; 8 | using Microsoft.Extensions.Options; 9 | using Xunit; 10 | 11 | namespace FluentEmail.Liquid.Tests.ComplexModel 12 | { 13 | public class ComplexModelRenderTests 14 | { 15 | [Fact] 16 | public void Can_Render_Complex_Model_Properties() 17 | { 18 | var model = new ParentModel 19 | { 20 | ParentName = new NameDetails { Firstname = "Luke", Surname = "Dinosaur" }, 21 | ChildrenNames = 22 | [ 23 | new NameDetails { Firstname = "ChildFirstA", Surname = "ChildLastA" }, 24 | new NameDetails { Firstname = "ChildFirstB", Surname = "ChildLastB" } 25 | ] 26 | }; 27 | 28 | var expected = @" 29 | Parent: Luke 30 | Children: 31 | 32 | * ChildFirstA ChildLastA 33 | * ChildFirstB ChildLastB 34 | "; 35 | 36 | var email = Email 37 | .From(TestData.FromEmail) 38 | .To(TestData.ToEmail) 39 | .Subject(TestData.Subject); 40 | 41 | email.Renderer = SetupRenderer(); 42 | 43 | email.UsingTemplate(Template(), model); 44 | 45 | email.Data.Body.Should().Be(expected); 46 | } 47 | 48 | [SuppressMessage("ReSharper", "StringLiteralTypo")] 49 | private string Template() 50 | { 51 | return @" 52 | Parent: {{ ParentName.Firstname }} 53 | Children: 54 | {% for Child in ChildrenNames %} 55 | * {{ Child.Firstname }} {{ Child.Surname }}{% endfor %} 56 | "; 57 | } 58 | 59 | private static ITemplateRenderer SetupRenderer( 60 | IFileProvider fileProvider = null, 61 | Action configureTemplateContext = null) 62 | { 63 | var options = new LiquidRendererOptions 64 | { 65 | FileProvider = fileProvider, 66 | ConfigureTemplateContext = configureTemplateContext, 67 | TemplateOptions = new TemplateOptions { MemberAccessStrategy = new UnsafeMemberAccessStrategy() } 68 | }; 69 | return new LiquidRenderer(Options.Create(options)); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /test/FluentEmail.Bootstrap.Tests/BootstrapTests.CompileBootstrap_Compiles.verified.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/FluentEmail.Bootstrap.Tests/BootstrapTests.UsingBootstrapBody_Compiles.verified.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/FluentEmail.Bootstrap.Tests/BootstrapTests.UsingBootstrapTemplate_Compiles.verified.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/FluentEmail.Core/EmbeddedTemplates.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace FluentEmail.Core; 5 | 6 | public static class EmbeddedTemplates 7 | { 8 | private static Assembly _assembly; 9 | private static string _rootPath; 10 | 11 | public static void Configure(Assembly assembly, string rootPath) 12 | { 13 | _assembly = assembly; 14 | _rootPath = rootPath; 15 | } 16 | 17 | /// 18 | /// Adds template to email from previously configured default embedded resource 19 | /// 20 | /// 21 | /// Path the the embedded resource eg [YourResourceFolder].[YourFilename.txt]. Will be appended to configured root path 22 | /// Model for the template 23 | /// True if Body is HTML (default), false for plain text 24 | /// 25 | public static IFluentEmail UsingTemplateFromEmbedded(this IFluentEmail email, string path, T model, bool isHtml = true) 26 | { 27 | if (_assembly is null) 28 | { 29 | throw new Exception("FluentEmail.Core.EmbeddedTemplates.Configure must be called with default assembly and root path"); 30 | } 31 | 32 | var root = _rootPath; 33 | if (!string.IsNullOrEmpty(root)) root += "."; 34 | var template = EmbeddedResourceHelper.GetResourceAsString(_assembly, $"{root}{path}"); 35 | var result = email.Renderer.Parse(template, model, isHtml); 36 | email.Data.IsHtml = isHtml; 37 | email.Data.Body = result; 38 | 39 | return email; 40 | } 41 | 42 | /// 43 | /// Adds alternative plaintext template to email from previously configured embedded resource 44 | /// 45 | /// 46 | /// Path the the embedded resource eg [YourResourceFolder].[YourFilename.txt]. Will be appended to configured root path 47 | /// Model for the template 48 | /// 49 | public static IFluentEmail PlaintextAlternativeUsingTemplateFromEmbedded(this IFluentEmail email, string path, T model) 50 | { 51 | var root = _rootPath; 52 | if (!string.IsNullOrEmpty(root)) root += "."; 53 | var template = EmbeddedResourceHelper.GetResourceAsString(_assembly, $"{root}{path}"); 54 | var result = email.Renderer.Parse(template, model, false); 55 | email.Data.PlaintextAlternativeBody = result; 56 | 57 | return email; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Postmark/FluentEmailExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using FluentEmail.Core.Models; 6 | 7 | namespace FluentEmail.Postmark 8 | { 9 | internal static class FluentEmailExtensions 10 | { 11 | public static string ToPmAddressString(this IList
addresses) 12 | { 13 | var adrStrs = addresses?.Select(a => a.EmailAddress).Where(s => !string.IsNullOrWhiteSpace(s)).ToList(); 14 | if (adrStrs == null || adrStrs.Count == 0) return null; 15 | if (adrStrs.Count > 50) throw new ArgumentException("Postmark does not support sending to more than 50 recipients at once"); 16 | return string.Join(",", adrStrs); 17 | } 18 | 19 | public static IEnumerable GetPmHeaders(this Priority value) 20 | { 21 | foreach ((var name, var val) in value.GetMailHeaders()) 22 | { 23 | yield return new PostmarkDotNet.Model.MailHeader(name, val); 24 | } 25 | } 26 | 27 | public static IEnumerable<(string name, string value)> GetMailHeaders(this Priority value) 28 | { 29 | // based on https://github.com/lukencode/FluentEmail/blob/master/src/Senders/FluentEmail.SendGrid/SendGridSender.cs 30 | switch (value) 31 | { 32 | case Priority.High: 33 | // https://stackoverflow.com/questions/23230250/set-email-priority-with-sendgrid-api 34 | yield return ("Priority", "Urgent"); 35 | yield return ("Importance", "High"); 36 | // https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmail/2bb19f1b-b35e-4966-b1cb-1afd044e83ab 37 | yield return ("X-Priority", "1"); 38 | yield return ("X-MSMail-Priority", "High"); 39 | break; 40 | 41 | case Priority.Low: 42 | // https://stackoverflow.com/questions/23230250/set-email-priority-with-sendgrid-api 43 | yield return ("Priority", "Non-Urgent"); 44 | yield return ("Importance", "Low"); 45 | // https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmail/2bb19f1b-b35e-4966-b1cb-1afd044e83ab 46 | yield return ("X-Priority", "5"); 47 | yield return ("X-MSMail-Priority", "Low"); 48 | break; 49 | 50 | case Priority.Normal: 51 | default: 52 | break; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/FluentEmail.ThirdParty.Tests/_Credentials.cs: -------------------------------------------------------------------------------- 1 | using dotenv.net; 2 | using dotenv.net.Utilities; 3 | 4 | namespace FluentEmail.Core.Tests; 5 | 6 | internal static class Credentials 7 | { 8 | public static TestCredentials MailTrap => new ("MAILTRAP"); 9 | public static MailgunCredentials Mailgun => new("MAILGUN"); 10 | public static TestCredentials Azure => new ("AZURE"); 11 | public static TestCredentials Postmark => new("POSTMARK"); 12 | public static TestCredentials SendGrid => new("SENDGRID"); 13 | public static GraphCredentials Graph => new("GRAPH"); 14 | 15 | public static string ToEmail; 16 | public static string FromEmail; 17 | 18 | static Credentials() 19 | { 20 | DotEnv.Load(); 21 | ToEmail = GetDotEnvString("FE_TEST_TO_EMAIL"); 22 | FromEmail = GetDotEnvString("FE_TEST_FROM_EMAIL"); 23 | } 24 | 25 | public static string GetDotEnvString(string key) => EnvReader.TryGetStringValue(key, out var value) ? value : null; 26 | 27 | public static int? GetDotEnvInt(string key) => EnvReader.TryGetIntValue(key, out var value) ? value : null; 28 | } 29 | 30 | internal class TestCredentials(string keyBase) 31 | { 32 | public string Host { get; private set; } = Credentials.GetDotEnvString($"FE_TEST_{keyBase}_HOST"); 33 | public string User { get; private set; } = Credentials.GetDotEnvString($"FE_TEST_{keyBase}_USER"); 34 | public string Password { get; private set; } = Credentials.GetDotEnvString($"FE_TEST_{keyBase}_PWD"); 35 | public int? Port { get; private set; } = Credentials.GetDotEnvInt($"FE_TEST_{keyBase}_PORT") ?? 587; 36 | public string ApiKey { get; private set; } = Credentials.GetDotEnvString($"FE_TEST_{keyBase}_API_KEY"); 37 | public string ApiHost { get; private set; } = Credentials.GetDotEnvString($"FE_TEST_{keyBase}_API_HOST"); 38 | public string Template { get; private set; } = Credentials.GetDotEnvString($"FE_TEST_{keyBase}_TEMPLATE"); 39 | public string FromEmail { get; private set; } = Credentials.GetDotEnvString($"FE_TEST_{keyBase}_FROM_EMAIL"); 40 | public string ToEmail { get; private set; } = Credentials.GetDotEnvString($"FE_TEST_{keyBase}_TO_EMAIL"); 41 | } 42 | 43 | internal class MailgunCredentials(string keyBase) : TestCredentials(keyBase) 44 | { 45 | public string Domain { get; set; } = Credentials.GetDotEnvString($"FE_TEST_{keyBase}_DOMAIN"); 46 | } 47 | 48 | internal class GraphCredentials(string keyBase) : TestCredentials(keyBase) 49 | { 50 | public string AppId { get; set; } = Credentials.GetDotEnvString($"FE_TEST_{keyBase}_APP_ID"); 51 | public string TenantId { get; set; } = Credentials.GetDotEnvString($"FE_TEST_{keyBase}_TENANT_ID"); 52 | public string ClientSecret { get; set; } = Credentials.GetDotEnvString($"FE_TEST_{keyBase}_CLIENT_SECRET"); 53 | } -------------------------------------------------------------------------------- /src/Renderers/FluentEmail.Liquid/LiquidParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text.Encodings.Web; 6 | using System.Threading.Tasks; 7 | using Fluid; 8 | using Fluid.Ast; 9 | 10 | public class LiquidParser : FluidParser 11 | { 12 | public LiquidParser() 13 | { 14 | RegisterExpressionTag("layout", OnRegisterLayoutTag); 15 | RegisterEmptyTag("renderbody", OnRegisterRenderBodyTag); 16 | RegisterIdentifierBlock("section", OnRegisterSectionBlock); 17 | RegisterIdentifierTag("rendersection", OnRegisterSectionTag); 18 | } 19 | 20 | private async ValueTask OnRegisterLayoutTag(Expression expression, TextWriter writer, TextEncoder encoder, TemplateContext context) 21 | { 22 | const string viewExtension = ".liquid"; 23 | 24 | var relativeLayoutPath = (await expression.EvaluateAsync(context)).ToStringValue(); 25 | 26 | if (!relativeLayoutPath.EndsWith(viewExtension, StringComparison.OrdinalIgnoreCase)) 27 | { 28 | relativeLayoutPath += viewExtension; 29 | } 30 | 31 | context.AmbientValues["Layout"] = relativeLayoutPath; 32 | 33 | return Completion.Normal; 34 | } 35 | 36 | private async ValueTask OnRegisterRenderBodyTag(TextWriter writer, TextEncoder encoder, TemplateContext context) 37 | { 38 | static void ThrowParseException() 39 | { 40 | throw new ParseException("Could not render body, Layouts can't be evaluated directly."); 41 | } 42 | 43 | if (context.AmbientValues.TryGetValue("Body", out var body)) 44 | { 45 | await writer.WriteAsync((string)body); 46 | } 47 | else 48 | { 49 | ThrowParseException(); 50 | } 51 | 52 | return Completion.Normal; 53 | } 54 | 55 | private ValueTask OnRegisterSectionBlock(string sectionName, IReadOnlyList statements, TextWriter writer, TextEncoder encoder, TemplateContext context) 56 | { 57 | if (context.AmbientValues.TryGetValue("Sections", out var sections)) 58 | { 59 | var dictionary = (Dictionary>) sections; 60 | 61 | dictionary[sectionName] = statements.ToList(); 62 | } 63 | 64 | return new ValueTask(Completion.Normal); 65 | } 66 | 67 | private async ValueTask OnRegisterSectionTag(string sectionName, TextWriter writer, TextEncoder encoder, TemplateContext context) 68 | { 69 | if (context.AmbientValues.TryGetValue("Sections", out var sections)) 70 | { 71 | var dictionary = (Dictionary>) sections; 72 | 73 | if (dictionary.TryGetValue(sectionName, out var section)) 74 | { 75 | foreach(var statement in section) 76 | { 77 | await statement.WriteToAsync(writer, encoder, context); 78 | } 79 | } 80 | } 81 | 82 | return Completion.Normal; 83 | } 84 | } -------------------------------------------------------------------------------- /src/Renderers/FluentEmail.Liquid/FluentEmailFluidBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using FluentEmail.Core; 4 | using FluentEmail.Core.Interfaces; 5 | using FluentEmail.Liquid; 6 | 7 | using Microsoft.Extensions.DependencyInjection.Extensions; 8 | using Microsoft.Extensions.FileProviders; 9 | 10 | // ReSharper disable once CheckNamespace 11 | namespace Microsoft.Extensions.DependencyInjection 12 | { 13 | public static class FluentEmailFluidBuilderExtensions 14 | { 15 | public static FluentEmailServicesBuilder AddLiquidRenderer( 16 | this FluentEmailServicesBuilder builder, 17 | Action? configure = null) 18 | { 19 | builder.Services.AddOptions(); 20 | if (configure != null) 21 | { 22 | builder.Services.Configure(configure); 23 | } 24 | 25 | builder.Services.TryAddSingleton(); 26 | return builder; 27 | } 28 | 29 | public static FluentEmailServicesBuilder AddLiquidRendererWithEmbedded( 30 | this FluentEmailServicesBuilder builder, 31 | Action? configure = null) 32 | { 33 | var assembly = Assembly.GetCallingAssembly(); 34 | var name = assembly.GetName().Name; 35 | return AddLiquidRendererWithEmbedded(builder, assembly, $"{name}.EmailTemplates", configure); 36 | } 37 | 38 | public static FluentEmailServicesBuilder AddLiquidRendererWithEmbedded( 39 | this FluentEmailServicesBuilder builder, 40 | Assembly assembly, 41 | Action? configure = null) 42 | { 43 | var name = assembly.GetName().Name; 44 | return AddLiquidRendererWithEmbedded(builder, assembly, $"{name}.EmailTemplates", configure); 45 | } 46 | 47 | public static FluentEmailServicesBuilder AddLiquidRendererWithEmbedded( 48 | this FluentEmailServicesBuilder builder, 49 | string rootPath, 50 | Action? configure = null) 51 | { 52 | var assembly = Assembly.GetCallingAssembly(); 53 | var name = assembly.GetName().Name; 54 | if (!string.IsNullOrEmpty(rootPath)) name += "."; 55 | return AddLiquidRendererWithEmbedded(builder, assembly, $"{name}{rootPath}", configure); 56 | } 57 | 58 | public static FluentEmailServicesBuilder AddLiquidRendererWithEmbedded( 59 | this FluentEmailServicesBuilder builder, 60 | Assembly assembly, 61 | string rootNamespace, 62 | Action? configure = null) 63 | { 64 | builder.AddLiquidRenderer(options => 65 | { 66 | options.FileProvider = new EmbeddedFileProvider(assembly, rootNamespace); 67 | configure?.Invoke(options); 68 | }); 69 | EmbeddedTemplates.Configure(assembly, rootNamespace); 70 | return builder; 71 | } 72 | 73 | } 74 | } -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Postmark/FluentEmailPostmarkBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using FluentEmail.Core.Interfaces; 5 | using FluentEmail.Postmark; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.DependencyInjection.Extensions; 8 | 9 | namespace Microsoft.Extensions.DependencyInjection 10 | { 11 | /// 12 | /// Provides fluent extension methods for adding Postmark as a sending backend to FluentEmail. 13 | /// 14 | public static class FluentEmailPostmarkBuilderExtensions 15 | { 16 | /// 17 | /// Adds a PostmarkSender to be used by FluentEmail. 18 | /// 19 | /// The builder for this FluentEmail service. 20 | /// The serverToken to use to authenticate at the Postmark API. 21 | public static FluentEmailServicesBuilder AddPostmarkSender(this FluentEmailServicesBuilder builder, string serverToken) 22 | { 23 | _ = builder ?? throw new ArgumentNullException(nameof(builder)); 24 | builder.Services.TryAdd(ServiceDescriptor.Scoped(x => new PostmarkSender(serverToken))); 25 | return builder; 26 | } 27 | 28 | /// 29 | /// Adds a PostmarkSender to be used by FluentEmail. 30 | /// 31 | /// The builder for this FluentEmail service. 32 | /// The serverToken to use to authenticate at the Postmark API. 33 | /// A method that changes the desired options of a PostmarkSenderOptions instance. 34 | public static FluentEmailServicesBuilder AddPostmarkSender(this FluentEmailServicesBuilder builder, string serverToken, Action configureOptions) 35 | { 36 | _ = builder ?? throw new ArgumentNullException(nameof(builder)); 37 | _ = configureOptions ?? throw new ArgumentNullException(nameof(configureOptions)); 38 | var opts = new PostmarkSenderOptions(serverToken); 39 | configureOptions(opts); 40 | builder.Services.TryAdd(ServiceDescriptor.Scoped(x => new PostmarkSender(opts))); 41 | return builder; 42 | } 43 | 44 | /// 45 | /// Adds a PostmarkSender to be used by FluentEmail. 46 | /// 47 | /// The builder for this FluentEmail service. 48 | /// A preconfigured PostmarkSenderOptions instance. 49 | public static FluentEmailServicesBuilder AddPostmarkSender(this FluentEmailServicesBuilder builder, PostmarkSenderOptions options) 50 | { 51 | _ = builder ?? throw new ArgumentNullException(nameof(builder)); 52 | _ = options ?? throw new ArgumentNullException(nameof(options)); 53 | builder.Services.TryAdd(ServiceDescriptor.Scoped(x => new PostmarkSender(options))); 54 | return builder; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Exchange/ExchangeSender.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core; 2 | using FluentEmail.Core.Interfaces; 3 | using FluentEmail.Core.Models; 4 | using Microsoft.Exchange.WebServices.Data; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace FluentEmail.Exchange 9 | { 10 | public class ExchangeSender : ISender 11 | { 12 | private readonly ExchangeService meExchangeClient; 13 | 14 | public ExchangeSender(ExchangeService paExchangeClient) 15 | { 16 | meExchangeClient = paExchangeClient; 17 | } 18 | 19 | public SendResponse Send(IFluentEmail email, CancellationToken? token = null) 20 | { 21 | return System.Threading.Tasks.Task.Run(() => SendAsync(email, token)).Result; 22 | } 23 | 24 | public async Task SendAsync(IFluentEmail email, CancellationToken? token = null) 25 | { 26 | var response = new SendResponse(); 27 | 28 | var message = CreateMailMessage(email); 29 | 30 | if (token?.IsCancellationRequested ?? false) 31 | { 32 | response.ErrorMessages.Add("Message was cancelled by cancellation token."); 33 | return response; 34 | } 35 | 36 | message.Save(); 37 | message.SendAndSaveCopy(); 38 | 39 | return response; 40 | } 41 | 42 | private EmailMessage CreateMailMessage(IFluentEmail paEmail) 43 | { 44 | var paData = paEmail.Data; 45 | 46 | EmailMessage loExchangeMessage = new EmailMessage(meExchangeClient) 47 | { 48 | Subject = paData.Subject, 49 | Body = paData.Body, 50 | }; 51 | 52 | if (!string.IsNullOrEmpty(paData.FromAddress?.EmailAddress)) 53 | loExchangeMessage.From = new EmailAddress(paData.FromAddress.Name, paData.FromAddress.EmailAddress); 54 | 55 | paData.ToAddresses.ForEach(x => 56 | { 57 | loExchangeMessage.ToRecipients.Add(new EmailAddress(x.Name, x.EmailAddress)); 58 | }); 59 | 60 | paData.CcAddresses.ForEach(x => 61 | { 62 | loExchangeMessage.CcRecipients.Add(new EmailAddress(x.Name, x.EmailAddress)); 63 | }); 64 | 65 | paData.BccAddresses.ForEach(x => 66 | { 67 | loExchangeMessage.BccRecipients.Add(new EmailAddress(x.EmailAddress, x.Name)); 68 | }); 69 | 70 | switch (paData.Priority) 71 | { 72 | case Priority.Low: 73 | loExchangeMessage.Importance = Importance.Low; 74 | break; 75 | 76 | case Priority.Normal: 77 | loExchangeMessage.Importance = Importance.Normal; 78 | break; 79 | 80 | case Priority.High: 81 | loExchangeMessage.Importance = Importance.High; 82 | break; 83 | } 84 | 85 | paData.Attachments.ForEach(x => 86 | { 87 | // System.Net.Mail.Attachment a = new System.Net.Mail.Attachment(x.Data, x.Filename, x.ContentType); 88 | // a.ContentId = x.ContentId; 89 | loExchangeMessage.Attachments.AddFileAttachment(x.Filename, x.Data); 90 | }); 91 | 92 | return loExchangeMessage; 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /src/Renderers/FluentEmail.Liquid/LiquidRenderer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using FluentEmail.Core.Interfaces; 6 | using Fluid; 7 | using Fluid.Ast; 8 | using Microsoft.Extensions.FileProviders; 9 | using Microsoft.Extensions.Options; 10 | 11 | namespace FluentEmail.Liquid 12 | { 13 | public class LiquidRenderer : ITemplateRenderer 14 | { 15 | private readonly IOptions _options; 16 | private readonly LiquidParser _parser; 17 | 18 | public LiquidRenderer(IOptions options) 19 | { 20 | _options = options; 21 | _parser = new LiquidParser(); 22 | _options.Value.ConfigureParser?.Invoke(_parser); 23 | } 24 | 25 | public string Parse(string template, T model, bool isHtml = true) 26 | { 27 | return ParseAsync(template, model, isHtml).GetAwaiter().GetResult(); 28 | } 29 | 30 | public async Task ParseAsync(string template, T model, bool isHtml = true) 31 | { 32 | var rendererOptions = _options.Value; 33 | 34 | // Check for a custom file provider 35 | var fileProvider = rendererOptions.FileProvider; 36 | var viewTemplate = ParseTemplate(template); 37 | 38 | var context = new TemplateContext(model, rendererOptions.TemplateOptions) 39 | { 40 | // provide some services to all statements 41 | AmbientValues = 42 | { 43 | ["FileProvider"] = fileProvider, 44 | ["Sections"] = new Dictionary>() 45 | }, 46 | Options = 47 | { 48 | FileProvider = fileProvider 49 | } 50 | }; 51 | 52 | rendererOptions.ConfigureTemplateContext?.Invoke(context, model!); 53 | 54 | var body = await viewTemplate.RenderAsync(context, rendererOptions.TextEncoder); 55 | 56 | // if a layout is specified while rendering a view, execute it 57 | if (context.AmbientValues.TryGetValue("Layout", out var layoutPath)) 58 | { 59 | context.AmbientValues["Body"] = body; 60 | var layoutTemplate = ParseLiquidFile((string)layoutPath, fileProvider!); 61 | 62 | return await layoutTemplate.RenderAsync(context, rendererOptions.TextEncoder); 63 | } 64 | 65 | return body; 66 | } 67 | 68 | private IFluidTemplate ParseLiquidFile( 69 | string path, 70 | IFileProvider? fileProvider) 71 | { 72 | static void ThrowMissingFileProviderException() 73 | { 74 | const string message = "Cannot parse external file, file provider missing"; 75 | throw new ArgumentNullException(nameof(LiquidRendererOptions.FileProvider), message); 76 | } 77 | 78 | if (fileProvider is null) 79 | { 80 | ThrowMissingFileProviderException(); 81 | } 82 | 83 | var fileInfo = fileProvider!.GetFileInfo(path); 84 | using var stream = fileInfo.CreateReadStream(); 85 | using var sr = new StreamReader(stream); 86 | 87 | return ParseTemplate(sr.ReadToEnd()); 88 | } 89 | 90 | private IFluidTemplate ParseTemplate(string content) 91 | { 92 | if (!_parser.TryParse(content, out var template, out var errors)) 93 | { 94 | throw new Exception(string.Join(Environment.NewLine, errors)); 95 | } 96 | 97 | return template; 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /test/FluentEmail.ThirdParty.Tests/GraphSenderTests.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core; 2 | using FluentEmail.Core.Tests; 3 | using FluentEmail.Graph; 4 | 5 | namespace FluentEmail.ThirdParty.Tests; 6 | 7 | public class GraphSenderTests 8 | { 9 | private readonly string _appId = Credentials.Graph.AppId; 10 | private readonly string _tenantId = Credentials.Graph.TenantId; 11 | private readonly string _graphSecret = Credentials.Graph.ClientSecret; 12 | private readonly string _senderEmail = Credentials.Graph.FromEmail ?? Credentials.FromEmail; 13 | private readonly string _toEmail = Credentials.Graph.ToEmail ?? Credentials.ToEmail; 14 | private const bool SaveSent = false; 15 | 16 | private ISender Sender { get; } 17 | 18 | public GraphSenderTests() 19 | { 20 | if (string.IsNullOrWhiteSpace(_appId)) return; 21 | if (string.IsNullOrWhiteSpace(_tenantId)) return; 22 | if (string.IsNullOrWhiteSpace(_graphSecret)) return; 23 | if (string.IsNullOrWhiteSpace(_senderEmail)) return; 24 | 25 | Sender = new GraphSender(_appId, _tenantId, _graphSecret, SaveSent); 26 | } 27 | 28 | [Fact] 29 | public void CanSendEmail() 30 | { 31 | Assert.SkipWhen(string.IsNullOrEmpty(_appId), "No Graph/AD Credentials"); 32 | 33 | var email = Email 34 | .From(_senderEmail) 35 | .To(_toEmail) 36 | .Subject("Test Email") 37 | .Body("Test email from Graph sender unit test"); 38 | 39 | email.Sender = Sender; 40 | var response = email.Send(); 41 | (response.Successful).Should().BeTrue(); 42 | } 43 | 44 | [Fact] 45 | public async Task CanSendEmailAsync() 46 | { 47 | Assert.SkipWhen(string.IsNullOrEmpty(_appId), "No Graph/AD Credentials"); 48 | 49 | var email = Email 50 | .From(_senderEmail) 51 | .To(_toEmail) 52 | .Subject("Test Async Email") 53 | .Body("Test email from Graph sender unit test"); 54 | 55 | email.Sender = Sender; 56 | var response = await email.SendAsync(); 57 | (response.Successful).Should().BeTrue(); 58 | } 59 | 60 | [Fact] 61 | public async Task CanSendEmailWithAttachments() 62 | { 63 | Assert.SkipWhen(string.IsNullOrEmpty(_appId), "No Graph/AD Credentials"); 64 | 65 | var stream = new MemoryStream(); 66 | var sw = new StreamWriter(stream); 67 | await sw.WriteLineAsync("Hey this is some text in an attachment"); 68 | await sw.FlushAsync(TestContext.Current.CancellationToken); 69 | stream.Seek(0, SeekOrigin.Begin); 70 | 71 | var attachment = new Attachment 72 | { 73 | ContentType = "text/plain", 74 | Filename = "graphtest.txt", 75 | Data = stream 76 | }; 77 | 78 | var email = Email 79 | .From(_senderEmail) 80 | .To(_toEmail) 81 | .Subject("Test Email with Attachments") 82 | .Body("Test email from Graph sender unit test") 83 | .Attach(attachment); 84 | 85 | email.Sender = Sender; 86 | var response = await email.SendAsync(); 87 | (response.Successful).Should().BeTrue(); 88 | } 89 | 90 | [Fact] 91 | public async Task CanSendHighPriorityEmail() 92 | { 93 | Assert.SkipWhen(string.IsNullOrEmpty(_appId), "No Graph/AD Credentials"); 94 | 95 | var email = Email 96 | .From(_senderEmail) 97 | .To(_toEmail) 98 | .Subject("Test High Priority Email") 99 | .Body("Test email from Graph sender unit test") 100 | .HighPriority(); 101 | 102 | email.Sender = Sender; 103 | var response = await email.SendAsync(); 104 | (response.Successful).Should().BeTrue(); 105 | } 106 | } -------------------------------------------------------------------------------- /test/FluentEmail.Bootstrap.Tests/BootstrapTests.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable StringLiteralTypo 2 | // ReSharper disable UnusedAutoPropertyAccessor.Local 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using FluentEmail.Core; 7 | using FluentEmail.Core.Interfaces; 8 | using FluentEmail.Liquid; 9 | using Fluid; 10 | using Microsoft.Extensions.FileProviders; 11 | using Microsoft.Extensions.Options; 12 | using Xunit; 13 | using VerifyTests; 14 | using VerifyXunit; 15 | 16 | namespace FluentEmail.Bootstrap.Tests; 17 | 18 | public class BootstrapTests 19 | { 20 | private const string ToEmail = "bob@test.com"; 21 | private const string FromEmail = "johno@test.com"; 22 | private const string Subject = "sup dawg"; 23 | private readonly VerifySettings _settings = new(); 24 | 25 | public BootstrapTests() 26 | { 27 | _settings.ScrubLinesContaining("Compiled with Bootstrap Email DotNet"); 28 | _settings.DisableDiff(); 29 | } 30 | 31 | private static ITemplateRenderer SetupRenderer( 32 | IFileProvider fileProvider = null, 33 | Action configureTemplateContext = null) 34 | { 35 | var options = new LiquidRendererOptions 36 | { 37 | FileProvider = fileProvider, 38 | ConfigureTemplateContext = configureTemplateContext, 39 | }; 40 | return new LiquidRenderer(Options.Create(options)); 41 | } 42 | 43 | [Fact] 44 | public Task CompileBootstrap_Compiles() 45 | { 46 | var template = """ 47 | 48 | 49 | Hi {{ Name }} here is a list {% for i in Numbers %}{{ i }}{% endfor %} 50 | 51 | 52 | """; 53 | var email = new Email(FromEmail) 54 | { 55 | Renderer = SetupRenderer() 56 | } 57 | .To(ToEmail) 58 | .Subject(Subject) 59 | .UsingTemplate(template, new ViewModel { Name = "LUKE", Numbers = ["1", "2", "3"] }) 60 | .CompileBootstrap(); 61 | 62 | return Verifier.Verify(email.Data.Body, _settings); 63 | } 64 | 65 | [Fact] 66 | public Task UsingBootstrapBody_Compiles() 67 | { 68 | var body = """ 69 | 70 | 71 | This is simple text, no templating 72 | 73 | 74 | """; 75 | var email = new Email(FromEmail) 76 | { 77 | Renderer = SetupRenderer() 78 | } 79 | .To(ToEmail) 80 | .Subject(Subject) 81 | .UsingBootstrapBody(body); 82 | 83 | return Verifier.Verify(email.Data.Body, _settings); 84 | } 85 | 86 | 87 | [Fact] 88 | public Task UsingBootstrapTemplate_Compiles() 89 | { 90 | var template = """ 91 | 92 | 93 | Hi {{ Name }} here is a list {% for i in Numbers %}{{ i }}{% endfor %} 94 | 95 | 96 | """; 97 | var email = new Email(FromEmail) 98 | { 99 | Renderer = SetupRenderer() 100 | } 101 | .To(ToEmail) 102 | .Subject(Subject) 103 | .UsingBootstrapTemplate(template, new ViewModel { Name = "LUKE", Numbers = ["1", "2", "3"] }); 104 | 105 | return Verifier.Verify(email.Data.Body, _settings); 106 | } 107 | 108 | private class ViewModel 109 | { 110 | public string Name { get; set; } 111 | public string[] Numbers { get; set; } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/AddressTests.cs: -------------------------------------------------------------------------------- 1 | namespace FluentEmail.Core.Tests; 2 | 3 | public class AddressTests 4 | { 5 | [Fact] 6 | public void SplitAddress_Test() 7 | { 8 | var email = Email 9 | .From("test@test.com") 10 | .To("james@test.com;john@test.com", "James 1;John 2"); 11 | 12 | email.Data.ToAddresses.Count.Should().Be(2); 13 | email.Data.ToAddresses[0].EmailAddress.Should().Be("james@test.com"); 14 | email.Data.ToAddresses[1].EmailAddress.Should().Be("john@test.com"); 15 | email.Data.ToAddresses[0].Name.Should().Be("James 1"); 16 | email.Data.ToAddresses[1].Name.Should().Be("John 2"); 17 | } 18 | 19 | [Fact] 20 | public void SplitAddress_Test2() 21 | { 22 | var email = Email 23 | .From("test@test.com") 24 | .To("james@test.com; john@test.com", "James 1"); 25 | 26 | email.Data.ToAddresses.Count.Should().Be(2); 27 | email.Data.ToAddresses[0].EmailAddress.Should().Be("james@test.com"); 28 | email.Data.ToAddresses[1].EmailAddress.Should().Be("john@test.com"); 29 | email.Data.ToAddresses[0].Name.Should().Be("James 1"); 30 | email.Data.ToAddresses[1].Name.Should().Be(string.Empty); 31 | } 32 | 33 | [Fact] 34 | public void SplitAddress_Test3() 35 | { 36 | var email = Email 37 | .From("test@test.com") 38 | .To("james@test.com; john@test.com; Fred@test.com", "James 1;;Fred"); 39 | 40 | email.Data.ToAddresses.Count.Should().Be(3); 41 | email.Data.ToAddresses[0].EmailAddress.Should().Be("james@test.com"); 42 | email.Data.ToAddresses[1].EmailAddress.Should().Be("john@test.com"); 43 | email.Data.ToAddresses[2].EmailAddress.Should().Be("Fred@test.com"); 44 | email.Data.ToAddresses[0].Name.Should().Be("James 1"); 45 | email.Data.ToAddresses[1].Name.Should().Be(string.Empty); 46 | email.Data.ToAddresses[2].Name.Should().Be("Fred"); 47 | } 48 | 49 | [Fact] 50 | public void SetFromAddress() 51 | { 52 | var email = new Email(); 53 | email.SetFrom("test@test.test", "test"); 54 | 55 | email.Data.FromAddress.EmailAddress.Should().Be("test@test.test"); 56 | email.Data.FromAddress.Name.Should().Be("test"); 57 | } 58 | 59 | #region Refactored tests using setup through constructor. 60 | [Fact] 61 | public void New_SplitAddress_Test() 62 | { 63 | var email = new Email() 64 | .To("james@test.com;john@test.com", "James 1;John 2"); 65 | 66 | email.Data.ToAddresses.Count.Should().Be(2); 67 | email.Data.ToAddresses[0].EmailAddress.Should().Be("james@test.com"); 68 | email.Data.ToAddresses[1].EmailAddress.Should().Be("john@test.com"); 69 | email.Data.ToAddresses[0].Name.Should().Be("James 1"); 70 | email.Data.ToAddresses[1].Name.Should().Be("John 2"); 71 | } 72 | 73 | 74 | [Fact] 75 | public void New_SplitAddress_Test2() 76 | { 77 | var email = new Email() 78 | .To("james@test.com; john@test.com", "James 1"); 79 | 80 | email.Data.ToAddresses.Count.Should().Be(2); 81 | email.Data.ToAddresses[0].EmailAddress.Should().Be("james@test.com"); 82 | email.Data.ToAddresses[1].EmailAddress.Should().Be("john@test.com"); 83 | email.Data.ToAddresses[0].Name.Should().Be("James 1"); 84 | email.Data.ToAddresses[1].Name.Should().Be(string.Empty); 85 | } 86 | 87 | 88 | [Fact] 89 | public void New_SplitAddress_Test3() 90 | { 91 | var email = new Email() 92 | .To("james@test.com; john@test.com; Fred@test.com", "James 1;;Fred"); 93 | 94 | email.Data.ToAddresses.Count.Should().Be(3); 95 | email.Data.ToAddresses[0].EmailAddress.Should().Be("james@test.com"); 96 | email.Data.ToAddresses[1].EmailAddress.Should().Be("john@test.com"); 97 | email.Data.ToAddresses[2].EmailAddress.Should().Be("Fred@test.com"); 98 | email.Data.ToAddresses[0].Name.Should().Be("James 1"); 99 | email.Data.ToAddresses[1].Name.Should().Be(string.Empty); 100 | email.Data.ToAddresses[2].Name.Should().Be("Fred"); 101 | } 102 | #endregion 103 | } -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/SmtpSenderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Mail; 2 | using System.Threading; 3 | using FluentEmail.Smtp; 4 | 5 | namespace FluentEmail.Core.Tests; 6 | 7 | public class SmtpSenderTests 8 | { 9 | private const string ToEmail = "bob@test.com"; 10 | private const string FromEmail = "johno@test.com"; 11 | private const string Subject = "sup dawg"; 12 | private const string Body = "what be the hipitity hap?"; 13 | 14 | private static IFluentEmail TestEmail => Email 15 | .From(FromEmail) 16 | .To(ToEmail) 17 | .Subject(Subject) 18 | .Body(Body); 19 | 20 | private ISender GetSender(out string tempDirectory) 21 | { 22 | var path = Path.Combine(Path.GetTempPath(), Random.Shared.NextInt64().ToString(), "EmailTest"); 23 | 24 | var sender = new SmtpSender(() => new SmtpClient("localhost") 25 | { 26 | EnableSsl = false, 27 | DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory, 28 | PickupDirectoryLocation = path 29 | }); 30 | 31 | Directory.CreateDirectory(path); 32 | tempDirectory = path; 33 | return sender; 34 | } 35 | 36 | private void DeleteTemp(string tempDirectory) 37 | { 38 | try 39 | { 40 | Directory.Delete(tempDirectory, true); 41 | } 42 | // ReSharper disable once EmptyGeneralCatchClause 43 | catch 44 | { 45 | } 46 | } 47 | 48 | 49 | [Fact] 50 | public void CanSendEmail() 51 | { 52 | var email = TestEmail 53 | .Body("

Test

", true); 54 | 55 | email.Sender = GetSender(out var s); 56 | 57 | var response = email.Send(); 58 | 59 | var files = Directory.EnumerateFiles(s, "*.eml"); 60 | (response.Successful).Should().BeTrue(); 61 | (files).Should().NotBeEmpty(); 62 | DeleteTemp(s); 63 | 64 | } 65 | 66 | [Fact] 67 | public async Task CanSendEmailWithAttachments() 68 | { 69 | var stream = new MemoryStream(); 70 | var sw = new StreamWriter(stream); 71 | await sw.WriteLineAsync("Hey this is some text in an attachment"); 72 | await sw.FlushAsync(TestContext.Current.CancellationToken); 73 | stream.Seek(0, SeekOrigin.Begin); 74 | 75 | var attachment = new Attachment 76 | { 77 | Data = stream, 78 | ContentType = "text/plain", 79 | Filename = "mailgunTest.txt" 80 | }; 81 | 82 | var email = TestEmail 83 | .Attach(attachment); 84 | 85 | email.Sender = GetSender(out var s); 86 | 87 | var response = await email.SendAsync(); 88 | 89 | (response.Successful).Should().BeTrue(); 90 | var files = Directory.EnumerateFiles(s, "*.eml"); 91 | (files).Should().NotBeEmpty(); 92 | 93 | DeleteTemp(s); 94 | 95 | } 96 | 97 | [Fact] 98 | public async Task CanSendAsyncHtmlAndPlaintextTogether() 99 | { 100 | var email = TestEmail 101 | .Body("

Test

some body text

", true) 102 | .PlaintextAlternativeBody("Test - Some body text"); 103 | 104 | email.Sender = GetSender(out var s); 105 | 106 | var response = await email.SendAsync(); 107 | 108 | DeleteTemp(s); 109 | 110 | (response.Successful).Should().BeTrue(); 111 | } 112 | 113 | [Fact] 114 | public void CanSendHtmlAndPlaintextTogether() 115 | { 116 | var email = TestEmail 117 | .Body("

Test

some body text

", true) 118 | .PlaintextAlternativeBody("Test - Some body text"); 119 | 120 | email.Sender = GetSender(out var s); 121 | 122 | var response = email.Send(); 123 | 124 | DeleteTemp(s); 125 | 126 | (response.Successful).Should().BeTrue(); 127 | } 128 | 129 | [Fact] 130 | public void CancelSendIfCancellationRequested() 131 | { 132 | var email = TestEmail; 133 | 134 | var tokenSource = new CancellationTokenSource(); 135 | tokenSource.Cancel(); 136 | 137 | email.Sender = GetSender(out var s); 138 | 139 | var response = email.Send(tokenSource.Token); 140 | 141 | DeleteTemp(s); 142 | 143 | (response.Successful).Should().BeFalse(); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [4.0.0](https://github.com/jcamp-code/FluentEmail/compare/v3.8.0...v4.0.0) (2025-10-26) 4 | 5 | 6 | ### ⚠ BREAKING CHANGES 7 | 8 | * changed to .net 8.0, 9.0, 10.0 9 | 10 | ### ✨ Features 11 | 12 | * Added .net9.0 compatibility ([dbb57b4](https://github.com/jcamp-code/FluentEmail/commit/dbb57b4d90365b1f79cb8b1489ab40b7f6240426)) 13 | * Adds CheckCertificateRevocation to mailkit smtp options ([fed435e](https://github.com/jcamp-code/FluentEmail/commit/fed435e0abbafcea6f0597b4a17ef834b20e3645)), closes [#27](https://github.com/jcamp-code/FluentEmail/issues/27) 14 | * Changed to .net 8.0, 9.0, 10.0 ([e1034c4](https://github.com/jcamp-code/FluentEmail/commit/e1034c46cf215e15fa9fcc51948055da04e67e01)) 15 | 16 | 17 | ### 🏡 Miscellaneous Chores 18 | 19 | * Initial release-please setup ([#34](https://github.com/jcamp-code/FluentEmail/issues/34)) ([3657e64](https://github.com/jcamp-code/FluentEmail/commit/3657e6418637a1fc5988560d9dac2df03d8fc353)) 20 | * Update Bootstrap tests ([4a352fe](https://github.com/jcamp-code/FluentEmail/commit/4a352fea3fb36ce6e51c65d97b23f7aabb41ca1b)) 21 | * Update icons ([3292af1](https://github.com/jcamp-code/FluentEmail/commit/3292af18edd3e0ee93d6cd54c862b72efe05e2b3)) 22 | * Update RazorLight ([985766c](https://github.com/jcamp-code/FluentEmail/commit/985766cd491868e58f22b84be570a3c22c40f54e)), closes [#24](https://github.com/jcamp-code/FluentEmail/issues/24) 23 | * Update unit tests to xunit and Awesome Assertions ([#32](https://github.com/jcamp-code/FluentEmail/issues/32)) ([fe6f017](https://github.com/jcamp-code/FluentEmail/commit/fe6f017a82aa6bbd2911b61fa26e8dc527057466)) 24 | 25 | ## v3.8.0 26 | 27 | [compare changes](https://github.com/jcamp-code/FluentEmail/compare/v3.7.0...v3.8.0) 28 | 29 | ### 🚀 Enhancements 30 | 31 | - Update Mailkit to 4.7 and upgrade vulnerable components 32 | - Mailtrap support send with template method 33 | 34 | ### 🩹 Fixes 35 | - Update Mailkit to 4.7 and upgrade vulnerable components 36 | - Bind MailgunSender to ISender in singleton scope 37 | - Plaintext parameter to always include plaintext ([4a38382](https://github.com/jcamp-code/FluentEmail/commit/4a38382)) 38 | - Remove prerelease from azure sender ([90cac43](https://github.com/jcamp-code/FluentEmail/commit/90cac43)) 39 | - Email.AttachFromFilename does not dispose stream ([87441ae](https://github.com/jcamp-code/FluentEmail/commit/87441ae)) 40 | 41 | ### 🏡 Chore 42 | - Updated FluentEmail.MailerSend package reference in Readme 43 | - Tidy code ([8a24d6d](https://github.com/jcamp-code/FluentEmail/commit/8a24d6d)) 44 | 45 | ### ❤️ Contributors 46 | 47 | - [neo.zhu](https://github.com/neozhu) 48 | - [Aaron Sherber](https://github.com/asherber) 49 | - [Mark Menchavez](https://github.com/MarkMenchavez) 50 | - [marcoatribeiro](https://github.com/marcoatribeiro) 51 | - [brnn8r](https://github.com/brnn8r) 52 | - 53 | 54 | ## v3.7.0 55 | 56 | ### 🚀 Enhancements 57 | 58 | - Allow configuring Liquid parser ([#18](https://github.com/jcamp-code/FluentEmail/pull/18)) 59 | 60 | ### ❤️ Contributors 61 | 62 | - [Ville Häkli](https://github.com/VilleHakli) 63 | 64 | ## v3.6.1 65 | 66 | ### 🩹 Fixes 67 | 68 | - Use latest UnDotNet.BootstrapEmail ([f0fd690](https://github.com/jcamp-code/FluentEmail/commit/f0fd690)) 69 | 70 | ## v3.6.0 71 | 72 | ### 🚀 Enhancements 73 | 74 | - Update to latest Azure Email Client ([aa3a419](https://github.com/jcamp-code/FluentEmail/commit/aa3a419)) - thanks [@TheObliterator](https://github.com/TheObliterator) 75 | - Add UnDotNet.BootstrapEmail processing ([05cfca2](https://github.com/jcamp-code/FluentEmail/commit/05cfca2)) 76 | 77 | ### 🏡 Chore 78 | 79 | - Added README to all packages ([8801ddd](https://github.com/jcamp-code/FluentEmail/commit/8801ddd)) 80 | 81 | ## v3.5.1 82 | 83 | - Use GetCallingAssembly() rather than GetExecutingAssemby() in LiquidRenderer builder extensions 84 | 85 | ## v3.5.0 86 | 87 | - Added simplified configuration to setup and use embedded templates with and without the LiquidRenderer. 88 | 89 | ## v3.4.0 90 | 91 | - Added MailPace sender - thanks [@maartenba](https://github.com/maartenba) 92 | 93 | ## v3.3.1 94 | 95 | - Added MailKit builder to use injected config to allow it to come from .NET config system 96 | - Updated to MailKit 4.3.0 97 | 98 | ## v3.3 99 | 100 | - Added support for mailgun templates - [Original Source/Credit](https://github.com/gps-lasrol/FluentEmail/tree/support-mailgun-templates) 101 | - Fix Azure Email CC and BCC sending to the wrong email addresses - thanks [@megasware128](https://github.com/Megasware128) 102 | 103 | ## v3.2 104 | 105 | - Added FluentEmail.Postmark - [Original Source/Credit](https://github.com/georg-jung/FluentEmail.Postmark) 106 | 107 | ## v3.1 108 | 109 | - Initial release of jcamp.\* variants of FluentEmail 110 | -------------------------------------------------------------------------------- /test/FluentEmail.ThirdParty.Tests/AzureEmailSenderTests.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Azure.Email; 2 | using FluentEmail.Core; 3 | using FluentEmail.Core.Tests; 4 | 5 | namespace FluentEmail.ThirdParty.Tests; 6 | 7 | public class AzureEmailSenderTests 8 | { 9 | private readonly string _toEmail = Credentials.Azure.ToEmail ?? Credentials.ToEmail; 10 | private readonly string _fromEmail = Credentials.Azure.FromEmail ?? Credentials.FromEmail; 11 | private readonly string _connectionString = Credentials.Azure.ApiHost; 12 | 13 | private const string ToName = "FluentEmail tester"; 14 | private const string FromName = "AzureEmailSender Test"; 15 | 16 | private ISender Sender { get; } 17 | 18 | public AzureEmailSenderTests() 19 | { 20 | if (!string.IsNullOrEmpty(_connectionString)) Sender = new AzureEmailSender(_connectionString); 21 | } 22 | 23 | [Fact] 24 | public async Task CanSendEmail() 25 | { 26 | Assert.SkipWhen(string.IsNullOrEmpty(_connectionString), "No Azure Credentials"); 27 | 28 | const string subject = "SendMail Test"; 29 | const string body = "This email is testing send mail functionality of Azure Email Sender."; 30 | 31 | var email = Email 32 | .From(_fromEmail, FromName) 33 | .To(_toEmail, ToName) 34 | .Subject(subject) 35 | .Body(body); 36 | 37 | email.Sender = Sender; 38 | var response = await email.SendAsync(); 39 | 40 | (response.Successful).Should().BeTrue(); 41 | } 42 | 43 | [Fact] 44 | public async Task CanSendEmailWithReplyTo() 45 | { 46 | Assert.SkipWhen(string.IsNullOrEmpty(_connectionString), "No Azure Credentials"); 47 | 48 | const string subject = "SendMail Test"; 49 | const string body = "This email is testing send mail with ReplyTo functionality of Azure Email Sender."; 50 | 51 | var email = Email 52 | .From(_fromEmail, FromName) 53 | .To(_toEmail, ToName) 54 | .ReplyTo(_toEmail, ToName) 55 | .Subject(subject) 56 | .Body(body); 57 | 58 | email.Sender = Sender; 59 | var response = await email.SendAsync(); 60 | 61 | (response.Successful).Should().BeTrue(); 62 | } 63 | 64 | [Fact] 65 | public async Task CanSendEmailWithAttachments() 66 | { 67 | Assert.SkipWhen(string.IsNullOrEmpty(_connectionString), "No Azure Credentials"); 68 | 69 | const string subject = "SendMail With Attachments Test"; 70 | const string body = "This email is testing the attachment functionality of Azure Email Sender."; 71 | 72 | await using var stream = File.OpenRead($"{Directory.GetCurrentDirectory()}/test-binary.xlsx"); 73 | var attachment = new Attachment 74 | { 75 | Data = stream, 76 | ContentType = "xlsx", 77 | Filename = "test-binary.xlsx" 78 | }; 79 | 80 | var email = Email 81 | .From(_fromEmail, FromName) 82 | .To(_toEmail, ToName) 83 | .Subject(subject) 84 | .Body(body) 85 | .Attach(attachment); 86 | 87 | email.Sender = Sender; 88 | 89 | var response = await email.SendAsync(); 90 | 91 | (response.Successful).Should().BeTrue(); 92 | } 93 | 94 | [Fact] 95 | public async Task CanSendHighPriorityEmail() 96 | { 97 | Assert.SkipWhen(string.IsNullOrEmpty(_connectionString), "No Azure Credentials"); 98 | 99 | const string subject = "SendMail Test"; 100 | const string body = "This email is testing send mail functionality of Azure Email Sender."; 101 | 102 | var email = Email 103 | .From(_fromEmail, FromName) 104 | .To(_toEmail, ToName) 105 | .Subject(subject) 106 | .Body(body) 107 | .HighPriority(); 108 | 109 | email.Sender = Sender; 110 | var response = await email.SendAsync(); 111 | 112 | (response.Successful).Should().BeTrue(); 113 | } 114 | 115 | [Fact] 116 | public async Task CanSendLowPriorityEmail() 117 | { 118 | Assert.SkipWhen(string.IsNullOrEmpty(_connectionString), "No Azure Credentials"); 119 | 120 | const string subject = "SendMail Test"; 121 | const string body = "This email is testing send mail functionality of Azure Email Sender."; 122 | 123 | var email = Email 124 | .From(_fromEmail, FromName) 125 | .To(_toEmail, ToName) 126 | .Subject(subject) 127 | .Body(body) 128 | .LowPriority(); 129 | 130 | email.Sender = Sender; 131 | var response = await email.SendAsync(); 132 | 133 | (response.Successful).Should().BeTrue(); 134 | } 135 | } -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailtrap/HttpHelpers/HttpClientHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using System.Text.Json; 9 | using FluentEmail.Core; 10 | using System.Net.Http.Headers; 11 | 12 | namespace FluentEmail.Mailtrap.HttpHelpers 13 | { 14 | public class HttpClientHelpers 15 | { 16 | 17 | public static HttpContent GetJsonBody(object value) 18 | { 19 | return new StringContent(JsonSerializer.Serialize(value), Encoding.UTF8, "application/json"); 20 | } 21 | 22 | } 23 | 24 | public static class HttpClientExtensions 25 | { 26 | 27 | public static async Task> Post(this HttpClient client, string url, HttpContent httpContent) 28 | { 29 | var response = await client.PostAsync(url, httpContent); 30 | var qr = await QuickResponse.FromMessage(response); 31 | return qr.ToApiResponse(); 32 | } 33 | 34 | } 35 | 36 | public class QuickResponse 37 | { 38 | public HttpResponseMessage Message { get; set; } 39 | 40 | public string ResponseBody { get; set; } 41 | 42 | public IList Errors { get; set; } 43 | 44 | public QuickResponse() 45 | { 46 | Errors = new List(); 47 | } 48 | 49 | public ApiResponse ToApiResponse() 50 | { 51 | return new ApiResponse 52 | { 53 | Errors = Errors 54 | }; 55 | } 56 | 57 | public static async Task FromMessage(HttpResponseMessage message) 58 | { 59 | var response = new QuickResponse(); 60 | response.Message = message; 61 | response.ResponseBody = await message.Content.ReadAsStringAsync(); 62 | 63 | if (!message.IsSuccessStatusCode) 64 | { 65 | response.HandleFailedCall(); 66 | } 67 | 68 | return response; 69 | } 70 | 71 | protected void HandleFailedCall() 72 | { 73 | try 74 | { 75 | Errors = JsonSerializer.Deserialize>(ResponseBody) ?? new List(); 76 | 77 | if (!Errors.Any()) 78 | { 79 | Errors.Add(new ApiError 80 | { 81 | ErrorMessage = !string.IsNullOrEmpty(ResponseBody) ? ResponseBody : Message.StatusCode.ToString() 82 | }); 83 | } 84 | } 85 | catch (Exception) 86 | { 87 | Errors.Add(new ApiError 88 | { 89 | ErrorMessage = !string.IsNullOrEmpty(ResponseBody) ? ResponseBody : Message.StatusCode.ToString() 90 | }); 91 | } 92 | } 93 | } 94 | 95 | public class QuickResponse : QuickResponse 96 | { 97 | public T Data { get; set; } 98 | 99 | public new ApiResponse ToApiResponse() 100 | { 101 | return new ApiResponse 102 | { 103 | Errors = Errors, 104 | Data = Data 105 | }; 106 | } 107 | 108 | public new static async Task> FromMessage(HttpResponseMessage message) 109 | { 110 | var response = new QuickResponse(); 111 | response.Message = message; 112 | response.ResponseBody = await message.Content.ReadAsStringAsync(); 113 | 114 | if (message.IsSuccessStatusCode) 115 | { 116 | try 117 | { 118 | response.Data = JsonSerializer.Deserialize(response.ResponseBody); 119 | } 120 | catch (Exception) 121 | { 122 | response.HandleFailedCall(); 123 | } 124 | } 125 | else 126 | { 127 | response.HandleFailedCall(); 128 | } 129 | 130 | return response; 131 | } 132 | } 133 | 134 | public class QuickFile : QuickResponse 135 | { 136 | public new static async Task FromMessage(HttpResponseMessage message) 137 | { 138 | var response = new QuickFile(); 139 | response.Message = message; 140 | response.ResponseBody = await message.Content.ReadAsStringAsync(); 141 | 142 | if (message.IsSuccessStatusCode) 143 | { 144 | response.Data = await message.Content.ReadAsStreamAsync(); 145 | } 146 | else 147 | { 148 | response.HandleFailedCall(); 149 | } 150 | 151 | return response; 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /test/FluentEmail.ThirdParty.Tests/MailtrapSenderTests.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core; 2 | using FluentEmail.Core.Tests; 3 | using FluentEmail.Mailtrap; 4 | 5 | namespace FluentEmail.ThirdParty.Tests; 6 | 7 | public class MailtrapSenderTests 8 | { 9 | private const string Subject = "Mailtrap Email Test"; 10 | private const string Body = "This email is testing the functionality of mailtrap."; 11 | 12 | private readonly string _toEmail = Credentials.MailTrap.ToEmail ?? Credentials.ToEmail; 13 | private readonly string _fromEmail = Credentials.MailTrap.FromEmail ?? Credentials.FromEmail; 14 | private readonly string _host = Credentials.MailTrap.Host; 15 | private readonly string _username = Credentials.MailTrap.User; 16 | private readonly int _port = Credentials.MailTrap.Port ?? 587; 17 | private readonly string _password = Credentials.MailTrap.Password; 18 | private readonly string _apiHost = Credentials.MailTrap.ApiHost; 19 | private readonly string _apiKey = Credentials.MailTrap.ApiKey; 20 | private readonly string _templateId = Credentials.MailTrap.Template; 21 | 22 | private ISender Sender { get; } 23 | 24 | public MailtrapSenderTests() 25 | { 26 | if (!string.IsNullOrEmpty(_username)) Sender = new MailtrapSender(_username, _password, _host, _port); 27 | } 28 | 29 | [Fact] 30 | public void CanSendEmail() 31 | { 32 | Assert.SkipWhen(string.IsNullOrEmpty(_password), "No Mailtrap Credentials"); 33 | 34 | var email = Email 35 | .From(_fromEmail) 36 | .To(_toEmail) 37 | .Subject(Subject) 38 | .Body(Body); 39 | 40 | email.Sender = Sender; 41 | var response = email.Send(); 42 | 43 | (response.Successful).Should().BeTrue(); 44 | } 45 | 46 | 47 | [Fact] 48 | public async Task CanSendEmailAsync() 49 | { 50 | Assert.SkipWhen(string.IsNullOrEmpty(_password), "No Mailtrap Credentials"); 51 | 52 | var email = Email 53 | .From(_fromEmail) 54 | .To(_toEmail) 55 | .Subject(Subject) 56 | .Body(Body); 57 | 58 | email.Sender = Sender; 59 | var response = await email.SendAsync(); 60 | 61 | (response.Successful).Should().BeTrue(); 62 | } 63 | 64 | [Fact] 65 | public async Task CanSendEmailWithAttachments() 66 | { 67 | Assert.SkipWhen(string.IsNullOrEmpty(_password), "No Mailtrap Credentials"); 68 | 69 | var stream = new MemoryStream(); 70 | var sw = new StreamWriter(stream); 71 | await sw.WriteLineAsync("Hey this is some text in an attachment"); 72 | await sw.FlushAsync(TestContext.Current.CancellationToken); 73 | stream.Seek(0, SeekOrigin.Begin); 74 | 75 | var attachment = new Attachment 76 | { 77 | Data = stream, 78 | ContentType = "text/plain", 79 | Filename = "mailtrapTest.txt" 80 | }; 81 | 82 | var email = Email 83 | .From(_fromEmail) 84 | .To(_toEmail) 85 | .Subject(Subject) 86 | .Body(Body) 87 | .Attach(attachment); 88 | 89 | email.Sender = Sender; 90 | var response = await email.SendAsync(); 91 | 92 | (response.Successful).Should().BeTrue(); 93 | } 94 | 95 | [Fact] 96 | public async Task CanSendEmailWithInlineImages() 97 | { 98 | Assert.SkipWhen(string.IsNullOrEmpty(_password), "No Mailtrap Credentials"); 99 | 100 | await using var stream = File.OpenRead($"{Path.Combine(Directory.GetCurrentDirectory(), "logotest.png")}"); 101 | var attachment = new Attachment 102 | { 103 | IsInline = true, 104 | Data = stream, 105 | ContentType = "image/png", 106 | Filename = "logotest.png" 107 | }; 108 | 109 | var email = Email 110 | .From(_fromEmail) 111 | .To(_toEmail) 112 | .Subject(Subject) 113 | .Body("Inline image here: " + 114 | "

You should see an image without an attachment, or without a download prompt, depending on the email client.

", true) 115 | .Attach(attachment); 116 | 117 | email.Sender = Sender; 118 | var response = await email.SendAsync(); 119 | 120 | (response.Successful).Should().BeTrue(); 121 | } 122 | 123 | [Fact] 124 | public async Task CanSendEmailWithTemplate() 125 | { 126 | Assert.SkipWhen(string.IsNullOrEmpty(_apiKey), "No Mailtrap Credentials"); 127 | 128 | var email = Email.From(_fromEmail).To(_toEmail); 129 | email.Sender = new MailtrapSender(_username, _apiKey, _host, 587, _apiHost); 130 | 131 | // ReSharper disable once StringLiteralTypo 132 | var response = await email.SendWithTemplateAsync(_templateId, new { var1 = "Test", var2 = "VVVVVVVVVVVVV" }); 133 | 134 | (response.Successful).Should().BeTrue(); 135 | } 136 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | appsettings.development.json 10 | 11 | # User-specific files (MonoDevelop/Xamarin Studio) 12 | *.userprefs 13 | 14 | # Build results 15 | [Dd]ebug/ 16 | [Dd]ebugPublic/ 17 | [Rr]elease/ 18 | [Rr]eleases/ 19 | [Xx]64/ 20 | [Xx]86/ 21 | [Bb]uild/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | 26 | # Visual Studio 2015 cache/options directory 27 | .vs/ 28 | # Uncomment if you have tasks that create the project's static files in wwwroot 29 | #wwwroot/ 30 | 31 | # MSTest test Results 32 | [Tt]est[Rr]esult*/ 33 | [Bb]uild[Ll]og.* 34 | 35 | # NUNIT 36 | *.VisualState.xml 37 | TestResult.xml 38 | 39 | # Build Results of an ATL Project 40 | [Dd]ebugPS/ 41 | [Rr]eleasePS/ 42 | dlldata.c 43 | 44 | # DNX 45 | project.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | 144 | # TODO: Un-comment the next line if you do not want to checkin 145 | # your web deploy settings because they may include unencrypted 146 | # passwords 147 | #*.pubxml 148 | *.publishproj 149 | 150 | # NuGet Packages 151 | *.nupkg 152 | # The packages folder can be ignored because of Package Restore 153 | **/packages/* 154 | # except build/, which is used as an MSBuild target. 155 | !**/packages/build/ 156 | # Uncomment if necessary however generally it will be regenerated when needed 157 | #!**/packages/repositories.config 158 | # NuGet v3's project.json files produces more ignoreable files 159 | *.nuget.props 160 | *.nuget.targets 161 | 162 | # Microsoft Azure Build Output 163 | csx/ 164 | *.build.csdef 165 | 166 | # Microsoft Azure Emulator 167 | ecf/ 168 | rcf/ 169 | 170 | # Windows Store app package directory 171 | AppPackages/ 172 | BundleArtifacts/ 173 | 174 | # Visual Studio cache files 175 | # files ending in .cache can be ignored 176 | *.[Cc]ache 177 | # but keep track of directories ending in .cache 178 | !*.[Cc]ache/ 179 | 180 | # Others 181 | ClientBin/ 182 | [Ss]tyle[Cc]op.* 183 | ~$* 184 | *~ 185 | *.dbmdl 186 | *.dbproj.schemaview 187 | *.pfx 188 | *.publishsettings 189 | node_modules/ 190 | orleans.codegen.cs 191 | 192 | # RIA/Silverlight projects 193 | Generated_Code/ 194 | 195 | # Backup & report files from converting an old project file 196 | # to a newer Visual Studio version. Backup files are not needed, 197 | # because we have git ;-) 198 | _UpgradeReport_Files/ 199 | Backup*/ 200 | UpgradeLog*.XML 201 | UpgradeLog*.htm 202 | 203 | # SQL Server files 204 | *.mdf 205 | *.ldf 206 | 207 | # Business Intelligence projects 208 | *.rdl.data 209 | *.bim.layout 210 | *.bim_*.settings 211 | 212 | # Microsoft Fakes 213 | FakesAssemblies/ 214 | 215 | # GhostDoc plugin setting file 216 | *.GhostDoc.xml 217 | 218 | # Node.js Tools for Visual Studio 219 | .ntvs_analysis.dat 220 | 221 | # Visual Studio 6 build log 222 | *.plg 223 | 224 | # Visual Studio 6 workspace options file 225 | *.opt 226 | 227 | # Visual Studio LightSwitch build output 228 | **/*.HTMLClient/GeneratedArtifacts 229 | **/*.DesktopClient/GeneratedArtifacts 230 | **/*.DesktopClient/ModelManifest.xml 231 | **/*.Server/GeneratedArtifacts 232 | **/*.Server/ModelManifest.xml 233 | _Pvt_Extensions 234 | 235 | # LightSwitch generated files 236 | GeneratedArtifacts/ 237 | ModelManifest.xml 238 | 239 | # Paket dependency manager 240 | .paket/paket.exe 241 | 242 | # FAKE - F# Make 243 | .fake/ 244 | .vs 245 | .idea 246 | 247 | # Verify test files 248 | *.received.txt 249 | 250 | .env 251 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.MailPace/MailPaceSender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using FluentEmail.Core; 10 | using FluentEmail.Core.Interfaces; 11 | using FluentEmail.Core.Models; 12 | using Newtonsoft.Json; 13 | 14 | namespace FluentEmail.MailPace; 15 | 16 | public class MailPaceSender : ISender, IDisposable 17 | { 18 | private readonly HttpClient _httpClient; 19 | 20 | public MailPaceSender(string serverToken) 21 | { 22 | _httpClient = new HttpClient(); 23 | _httpClient.DefaultRequestHeaders.Add("MailPace-Server-Token", serverToken); 24 | _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 25 | } 26 | 27 | public SendResponse Send(IFluentEmail email, CancellationToken? token = null) => 28 | SendAsync(email, token).GetAwaiter().GetResult(); 29 | 30 | public async Task SendAsync(IFluentEmail email, CancellationToken? token = null) 31 | { 32 | var sendRequest = BuildSendRequestFor(email); 33 | 34 | var content = new StringContent(JsonConvert.SerializeObject(sendRequest), Encoding.UTF8, "application/json"); 35 | 36 | var response = await _httpClient.PostAsync("https://app.mailpace.com/api/v1/send", content) 37 | .ConfigureAwait(false); 38 | 39 | var mailPaceResponse = await TryOrNull(async () => JsonConvert.DeserializeObject( 40 | await response.Content.ReadAsStringAsync())) ?? new MailPaceResponse(); 41 | 42 | if (response.IsSuccessStatusCode) 43 | { 44 | return new SendResponse { MessageId = mailPaceResponse.Id }; 45 | } 46 | else 47 | { 48 | var result = new SendResponse(); 49 | 50 | if (!string.IsNullOrEmpty(mailPaceResponse.Error)) 51 | { 52 | result.ErrorMessages.Add(mailPaceResponse.Error); 53 | } 54 | 55 | if (mailPaceResponse.Errors != null && mailPaceResponse.Errors.Count != 0) 56 | { 57 | result.ErrorMessages.AddRange(mailPaceResponse.Errors 58 | .Select(it => $"{it.Key}: {string.Join("; ", it.Value)}")); 59 | } 60 | 61 | if (!result.ErrorMessages.Any()) 62 | { 63 | result.ErrorMessages.Add(response.ReasonPhrase ?? "An unknown error has occurred."); 64 | } 65 | 66 | return result; 67 | } 68 | } 69 | 70 | private static MailPaceSendRequest BuildSendRequestFor(IFluentEmail email) 71 | { 72 | var sendRequest = new MailPaceSendRequest 73 | { 74 | From = $"{email.Data.FromAddress.Name} <{email.Data.FromAddress.EmailAddress}>", 75 | To = string.Join(",", email.Data.ToAddresses.Select(it => !string.IsNullOrEmpty(it.Name) ? $"{it.Name} <{it.EmailAddress}>" : it.EmailAddress)), 76 | Subject = email.Data.Subject 77 | }; 78 | 79 | if (email.Data.CcAddresses.Any()) 80 | { 81 | sendRequest.Cc = string.Join(",", email.Data.CcAddresses.Select(it => !string.IsNullOrEmpty(it.Name) ? $"{it.Name} <{it.EmailAddress}>" : it.EmailAddress)); 82 | } 83 | 84 | if (email.Data.BccAddresses.Any()) 85 | { 86 | sendRequest.Bcc = string.Join(",", email.Data.BccAddresses.Select(it => !string.IsNullOrEmpty(it.Name) ? $"{it.Name} <{it.EmailAddress}>" : it.EmailAddress)); 87 | } 88 | 89 | if (email.Data.ReplyToAddresses.Any()) 90 | { 91 | sendRequest.ReplyTo = string.Join(",", email.Data.ReplyToAddresses.Select(it => !string.IsNullOrEmpty(it.Name) ? $"{it.Name} <{it.EmailAddress}>" : it.EmailAddress)); 92 | } 93 | 94 | if (email.Data.IsHtml) 95 | { 96 | sendRequest.HtmlBody = email.Data.Body; 97 | if (!string.IsNullOrEmpty(email.Data.PlaintextAlternativeBody)) 98 | { 99 | sendRequest.TextBody = email.Data.PlaintextAlternativeBody; 100 | } 101 | } 102 | else 103 | { 104 | sendRequest.TextBody = email.Data.Body; 105 | } 106 | 107 | if (email.Data.Tags.Any()) 108 | { 109 | sendRequest.Tags.AddRange(email.Data.Tags); 110 | } 111 | 112 | if (email.Data.Attachments.Any()) 113 | { 114 | sendRequest.Attachments.AddRange( 115 | email.Data.Attachments.Select(it => new MailPaceAttachment 116 | { 117 | Name = it.Filename, 118 | Content = it.Data.ConvertToBase64(), 119 | ContentType = it.ContentType ?? Path.GetExtension(it.Filename), // jpeg, jpg, png, gif, txt, pdf, docx, xlsx, pptx, csv, att, ics, ical, html, zip 120 | Cid = it.IsInline ? it.ContentId : null 121 | })); 122 | } 123 | 124 | return sendRequest; 125 | } 126 | 127 | private async Task TryOrNull(Func> method) 128 | { 129 | try 130 | { 131 | return await method(); 132 | } 133 | catch 134 | { 135 | return default; 136 | } 137 | } 138 | 139 | public void Dispose() 140 | { 141 | _httpClient.Dispose(); 142 | } 143 | } -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Postmark/PostmarkSender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using FluentEmail.Core; 7 | using FluentEmail.Core.Interfaces; 8 | using FluentEmail.Core.Models; 9 | using PostmarkDotNet; 10 | 11 | namespace FluentEmail.Postmark 12 | { 13 | /// 14 | /// A FluentEmail ISender implementation that uses postmark to send emails. 15 | /// 16 | public class PostmarkSender : ISender 17 | { 18 | private readonly PostmarkSenderOptions options; 19 | 20 | /// 21 | /// Creates a new instance of the PostmarkSender class. 22 | /// 23 | /// The serverToken to use to authenticate at the Postmark API. 24 | public PostmarkSender(string serverToken) : 25 | this(new PostmarkSenderOptions(serverToken ?? throw new ArgumentNullException(nameof(serverToken)))) 26 | { 27 | } 28 | 29 | /// 30 | /// Creates a new instance of the PostmarkSender class. 31 | /// 32 | /// The options to configure the behaviour of this sender. 33 | public PostmarkSender(PostmarkSenderOptions options) 34 | { 35 | this.options = options ?? throw new ArgumentNullException(nameof(options)); 36 | if (string.IsNullOrWhiteSpace(options.ServerToken)) 37 | throw new ArgumentException("The ServerToken property of the given options is null or whitespace but it is required to be set to a valid value.", nameof(options.ServerToken)); 38 | } 39 | 40 | /// 41 | /// Sends the email using the Postmark API. The implementation is async internally, this is a blocking wrapper. 42 | /// 43 | /// /// Returns a SendResponse instance that contains the Postmark ErrorCode as only ErrorMessage if the send was not successfull. 44 | public SendResponse Send(IFluentEmail email, CancellationToken? token = null) 45 | { 46 | // see https://stackoverflow.com/a/17284612/1200847 47 | // instead of .Result to not get an AggregateException 48 | return SendAsync(email, token).GetAwaiter().GetResult(); 49 | } 50 | 51 | /// 52 | /// Sends the email asynchronously using the Postmark API. 53 | /// 54 | /// Returns a SendResponse instance that contains the Postmark ErrorCode as only ErrorMessage if the send was not successfull. 55 | public async Task SendAsync(IFluentEmail email, CancellationToken? token = null) 56 | { 57 | _ = email ?? throw new ArgumentNullException(nameof(email)); 58 | var client = new PostmarkClient(options.ServerToken); 59 | var msg = CreatePostmarkMessage(email); 60 | var resp = await client.SendMessageAsync(msg).ConfigureAwait(false); 61 | return CreateSendResponse(resp); 62 | } 63 | 64 | private PostmarkMessage CreatePostmarkMessage(IFluentEmail email) 65 | { 66 | // see also https://postmarkapp.com/developer/api/email-api 67 | 68 | var msg = new PostmarkMessage 69 | { 70 | From = string.IsNullOrEmpty(email.Data.FromAddress.Name) ? 71 | email.Data.FromAddress.EmailAddress : 72 | $"{email.Data.FromAddress.Name} {email.Data.FromAddress.EmailAddress}", 73 | 74 | To = email.Data.ToAddresses.ToPmAddressString(), 75 | Cc = email.Data.CcAddresses.ToPmAddressString(), 76 | Bcc = email.Data.BccAddresses.ToPmAddressString(), 77 | ReplyTo = email.Data.ReplyToAddresses.ToPmAddressString(), 78 | 79 | Subject = email.Data.Subject 80 | }; 81 | 82 | var pmHeaderEnum = (email.Data.Headers ?? Enumerable.Empty>()) 83 | .Select(kvp => new PostmarkDotNet.Model.MailHeader(kvp.Key, kvp.Value)); 84 | pmHeaderEnum = pmHeaderEnum.Concat(email.Data.Priority.GetPmHeaders()); // add headers representing prio 85 | foreach (var pmh in pmHeaderEnum) 86 | { 87 | msg.Headers.Add(pmh); 88 | } 89 | 90 | if (email.Data.IsHtml) 91 | { 92 | msg.HtmlBody = email.Data.Body; 93 | msg.TextBody = email.Data.PlaintextAlternativeBody; 94 | } 95 | else 96 | { 97 | msg.TextBody = email.Data.Body; 98 | } 99 | 100 | foreach (var attachment in email.Data.Attachments ?? Enumerable.Empty()) 101 | { 102 | msg.AddAttachment(attachment.Data, attachment.Filename, attachment.ContentType, attachment.ContentId); 103 | } 104 | 105 | msg.TrackOpens = options.TrackOpens; 106 | msg.TrackLinks = options.TrackLinks; 107 | if (options.Tag != null) 108 | msg.Tag = options.Tag; 109 | if (options.Metadata != null) 110 | msg.Metadata = options.Metadata; 111 | 112 | return msg; 113 | } 114 | 115 | private static SendResponse CreateSendResponse(PostmarkResponse value) 116 | { 117 | var ret = new SendResponse(); 118 | ret.MessageId = value.MessageID.ToString(); 119 | if (value.Status == PostmarkStatus.Success) return ret; 120 | ret.ErrorMessages.Add(value.ErrorCode.ToString(System.Globalization.CultureInfo.InvariantCulture)); 121 | return ret; 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailtrap/MailtrapSender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Net.Http.Headers; 7 | using System.Net.Mail; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using FluentEmail.Core; 11 | using FluentEmail.Core.Models; 12 | using FluentEmail.Mailtrap.HttpHelpers; 13 | using FluentEmail.Smtp; 14 | 15 | namespace FluentEmail.Mailtrap 16 | { 17 | /// 18 | /// Send emails to a Mailtrap.io inbox 19 | /// 20 | public class MailtrapSender : IMailtrapSender, IDisposable 21 | { 22 | private readonly SmtpClient _smtpClient; 23 | private static readonly int[] ValidPorts = {25,587, 2525}; 24 | private readonly string _apiKey; 25 | private readonly string _apiHost; 26 | 27 | /// 28 | /// Creates a sender that uses the given Mailtrap credentials, but does not dispose it. 29 | /// 30 | /// Username of your mailtrap.io SMTP inbox 31 | /// Password of your mailtrap.io SMTP inbox 32 | /// Host address for the Mailtrap.io SMTP inbox 33 | /// Port for the Mailtrap.io SMTP server. Accepted values are 25, 465 or 2525. 34 | public MailtrapSender(string userName, string password, string host = "smtp.mailtrap.io", int? port = null, string apiHost = "https://send.api.mailtrap.io/api/send") 35 | { 36 | if (string.IsNullOrWhiteSpace(userName)) 37 | throw new ArgumentException("Mailtrap UserName needs to be supplied", nameof(userName)); 38 | 39 | if (string.IsNullOrWhiteSpace(password)) 40 | throw new ArgumentException("Mailtrap Password needs to be supplied", nameof(password)); 41 | 42 | if (port.HasValue && !ValidPorts.Contains(port.Value)) 43 | throw new ArgumentException("Mailtrap Port needs to be either 25, 465 or 2525", nameof(port)); 44 | _apiKey = password; 45 | _apiHost = apiHost; 46 | _smtpClient = new SmtpClient(host, port.GetValueOrDefault(587)) 47 | { 48 | Credentials = new NetworkCredential(userName, password), 49 | EnableSsl = true 50 | }; 51 | } 52 | 53 | public void Dispose() => _smtpClient?.Dispose(); 54 | 55 | public SendResponse Send(IFluentEmail email, CancellationToken? token = null) 56 | { 57 | var smtpSender = new SmtpSender(_smtpClient); 58 | return smtpSender.Send(email, token); 59 | } 60 | 61 | public Task SendAsync(IFluentEmail email, CancellationToken? token = null) 62 | { 63 | var smtpSender = new SmtpSender(_smtpClient); 64 | return smtpSender.SendAsync(email, token); 65 | } 66 | 67 | public async Task SendWithTemplateAsync(IFluentEmail email, string templateName, object templateData, CancellationToken? token = null) 68 | { 69 | token?.ThrowIfCancellationRequested(); 70 | using (var httpClient = new HttpClient { BaseAddress = new Uri(_apiHost) }) 71 | { 72 | httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey); 73 | httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 74 | var jsonContent = HttpClientHelpers.GetJsonBody(BuildMailtrapParameters(email, templateName, templateData)); 75 | var response = await httpClient.Post(_apiHost, jsonContent); 76 | var result = new SendResponse { MessageId = response.Data?.Id }; 77 | if (!response.Success) 78 | { 79 | result.ErrorMessages.AddRange(response.Errors.Select(x => x.ErrorMessage)); 80 | return result; 81 | } 82 | return result; 83 | } 84 | 85 | } 86 | 87 | private static Dictionary BuildMailtrapParameters(IFluentEmail email, string templateName, object templateData) 88 | { 89 | var parameters = new Dictionary(); 90 | parameters["from"] = new Dictionary{ 91 | {"email", email.Data.FromAddress.EmailAddress}, 92 | {"name", email.Data.FromAddress.Name} 93 | }; 94 | var to= new List>(); 95 | email.Data.ToAddresses.ForEach(x => 96 | { 97 | to.Add(new Dictionary { { "email", x.EmailAddress } }); 98 | }); 99 | parameters["to"] = to; 100 | var cc = new List>(); 101 | email.Data.CcAddresses.ForEach(x => 102 | { 103 | cc.Add(new Dictionary { { "email", x.EmailAddress } }); 104 | }); 105 | if (cc.Any()) 106 | { 107 | parameters["cc"] = to; 108 | } 109 | var bcc = new List>(); 110 | email.Data.BccAddresses.ForEach(x => 111 | { 112 | bcc.Add(new Dictionary { { "email", x.EmailAddress } }); 113 | }); 114 | if (cc.Any()) 115 | { 116 | parameters["bcc"] = bcc; 117 | } 118 | parameters["template_uuid"] = templateName; 119 | var templateVariables = templateData.GetType().GetProperties() 120 | .ToDictionary( 121 | prop => prop.Name, 122 | prop => prop.GetValue(templateData).ToString() 123 | ); 124 | parameters["template_variables"] = templateVariables; 125 | return parameters; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /test/FluentEmail.ThirdParty.Tests/MailgunSenderTests.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core; 2 | using FluentEmail.Core.Tests; 3 | using FluentEmail.Mailgun; 4 | using Newtonsoft.Json; 5 | 6 | namespace FluentEmail.ThirdParty.Tests; 7 | 8 | public class MailgunSenderTests 9 | { 10 | private readonly string _toEmail = Credentials.Mailgun.ToEmail ?? Credentials.ToEmail; 11 | private readonly string _fromEmail = Credentials.Mailgun.FromEmail ?? Credentials.FromEmail; 12 | private readonly string _apiKey = Credentials.Mailgun.ApiKey; 13 | private readonly string _domain = Credentials.Mailgun.Domain; 14 | private const string Subject = "Attachment Tests"; 15 | private const string Body = "This email is testing the attachment functionality of MailGun."; 16 | 17 | private ISender Sender { get; } 18 | 19 | public MailgunSenderTests() 20 | { 21 | if (!string.IsNullOrEmpty(_apiKey)) Sender = new MailgunSender(_domain, _apiKey); 22 | } 23 | 24 | [Fact] 25 | public async Task CanSendEmail() 26 | { 27 | Assert.SkipWhen(string.IsNullOrEmpty(_apiKey), "No Mailgun Credentials"); 28 | 29 | var email = Email 30 | .From(_fromEmail) 31 | .To(_toEmail) 32 | .Subject(Subject) 33 | .Body(Body); 34 | 35 | email.Sender = Sender; 36 | var response = await email.SendAsync(); 37 | 38 | (response.Successful).Should().BeTrue(); 39 | } 40 | 41 | [Fact] 42 | public async Task GetMessageIdInResponse() 43 | { 44 | Assert.SkipWhen(string.IsNullOrEmpty(_apiKey), "No Mailgun Credentials"); 45 | 46 | var email = Email 47 | .From(_fromEmail) 48 | .To(_toEmail) 49 | .Subject(Subject) 50 | .Body(Body); 51 | 52 | email.Sender = Sender; 53 | var response = await email.SendAsync(); 54 | 55 | (response.Successful).Should().BeTrue(); 56 | (response.MessageId).Should().NotBeEmpty(); 57 | } 58 | 59 | [Fact] 60 | public async Task CanSendEmailWithTag() 61 | { 62 | Assert.SkipWhen(string.IsNullOrEmpty(_apiKey), "No Mailgun Credentials"); 63 | 64 | var email = Email 65 | .From(_fromEmail) 66 | .To(_toEmail) 67 | .Subject(Subject) 68 | .Body(Body) 69 | .Tag("test"); 70 | 71 | email.Sender = Sender; 72 | var response = await email.SendAsync(); 73 | 74 | (response.Successful).Should().BeTrue(); 75 | } 76 | 77 | [Fact] 78 | public async Task CanSendEmailWithVariables() 79 | { 80 | Assert.SkipWhen(string.IsNullOrEmpty(_apiKey), "No Mailgun Credentials"); 81 | 82 | var email = Email 83 | .From(_fromEmail) 84 | .To(_toEmail) 85 | .Subject(Subject) 86 | .Body(Body) 87 | .Header("X-Mailgun-Variables", JsonConvert.SerializeObject(new Variable { Var1 = "Test"})); 88 | 89 | email.Sender = Sender; 90 | var response = await email.SendAsync(); 91 | 92 | (response.Successful).Should().BeTrue(); 93 | } 94 | 95 | [Fact] 96 | public async Task CanSendEmailWithAttachments() 97 | { 98 | Assert.SkipWhen(string.IsNullOrEmpty(_apiKey), "No Mailgun Credentials"); 99 | 100 | var stream = new MemoryStream(); 101 | var sw = new StreamWriter(stream); 102 | await sw.WriteLineAsync("Hey this is some text in an attachment"); 103 | await sw.FlushAsync(TestContext.Current.CancellationToken); 104 | stream.Seek(0, SeekOrigin.Begin); 105 | 106 | var attachment = new Attachment 107 | { 108 | Data = stream, 109 | ContentType = "text/plain", 110 | Filename = "mailgunTest.txt" 111 | }; 112 | 113 | var email = Email 114 | .From(_fromEmail) 115 | .To(_toEmail) 116 | .Subject(Subject) 117 | .Body(Body) 118 | .Attach(attachment); 119 | 120 | email.Sender = Sender; 121 | var response = await email.SendAsync(); 122 | 123 | (response.Successful).Should().BeTrue(); 124 | } 125 | 126 | [Fact] 127 | public async Task CanSendEmailWithInlineImages() 128 | { 129 | Assert.SkipWhen(string.IsNullOrEmpty(_apiKey), "No Mailgun Credentials"); 130 | 131 | await using var stream = File.OpenRead($"{Path.Combine(Directory.GetCurrentDirectory(), "logotest.png")}"); 132 | var attachment = new Attachment 133 | { 134 | IsInline = true, 135 | Data = stream, 136 | ContentType = "image/png", 137 | Filename = "logotest.png" 138 | }; 139 | 140 | var email = Email 141 | .From(_fromEmail) 142 | .To(_toEmail) 143 | .Subject(Subject) 144 | .Body("Inline image here: " + 145 | "

You should see an image without an attachment, or without a download prompt, depending on the email client.

", true) 146 | .Attach(attachment); 147 | 148 | email.Sender = Sender; 149 | var response = await email.SendAsync(); 150 | 151 | (response.Successful).Should().BeTrue(); 152 | } 153 | 154 | [Fact] 155 | public async Task CanSendEmailWithTemplate() 156 | { 157 | Assert.SkipWhen(string.IsNullOrEmpty(_apiKey), "No Mailgun Credentials"); 158 | Assert.SkipWhen(string.IsNullOrEmpty(Credentials.Mailgun.Template), "No Mailgun Template"); 159 | 160 | var email = Email 161 | .From(_fromEmail) 162 | .To(_toEmail) 163 | .Subject(Subject); 164 | 165 | email.Sender = Sender; 166 | var response = await email.SendWithTemplateAsync("test-template", new { var1 = "Test" }); 167 | 168 | (response.Successful).Should().BeTrue(); 169 | } 170 | 171 | private class Variable 172 | { 173 | // ReSharper disable once UnusedAutoPropertyAccessor.Local 174 | public string Var1 { get; set; } 175 | } 176 | } -------------------------------------------------------------------------------- /src/Renderers/FluentEmail.Bootstrap/FluentEmailFluidBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Reflection; 3 | using UnDotNet.BootstrapEmail; 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace FluentEmail.Core; 7 | 8 | public static class FluentEmailBootstrap 9 | { 10 | /// 11 | /// Compiles the template from the renderer through UnDotNet.BootstrapEmail 12 | /// 13 | /// The IFluentEmail object 14 | /// True if you want to set the plain text alternative automatically using UnDotNet.HtmlToText (Optional) 15 | /// Instance of the Email class 16 | // ReSharper disable once MemberCanBePrivate.Global 17 | public static IFluentEmail CompileBootstrap(this IFluentEmail email, bool plainText = true) 18 | { 19 | var compiler = new BootstrapCompiler(email.Data.Body); 20 | var result = compiler.Multipart(); 21 | email.Data.Body = result.Html; 22 | email.Data.IsHtml = true; 23 | if (plainText) 24 | { 25 | email.PlaintextAlternativeBody(result.Text); 26 | } 27 | 28 | return email; 29 | } 30 | 31 | /// 32 | /// Adds Bootstrap body to the email 33 | /// 34 | /// The IFluentEmail object 35 | /// The body 36 | /// True if Body is HTML, false for plain text (Optional) 37 | /// Instance of the Email class 38 | public static IFluentEmail UsingBootstrapBody(this IFluentEmail email, string body, bool isHtml = true) 39 | { 40 | email.Body(body, isHtml); 41 | email.CompileBootstrap(isHtml); 42 | return email; 43 | } 44 | 45 | /// 46 | /// Adds Bootstrap template to the email 47 | /// 48 | /// 49 | /// The IFluentEmail object 50 | /// The template 51 | /// Model for the template 52 | /// True if Body is HTML, false for plain text (Optional) 53 | /// Instance of the Email class 54 | public static IFluentEmail UsingBootstrapTemplate(this IFluentEmail email, string template, T model, 55 | bool isHtml = true) 56 | { 57 | email.UsingTemplate(template, model, isHtml); 58 | email.CompileBootstrap(isHtml); 59 | return email; 60 | } 61 | 62 | /// 63 | /// Adds Bootstrap template to email from embedded resource 64 | /// 65 | /// 66 | /// The IFluentEmail object 67 | /// Path the the embedded resource eg [YourAssembly].[YourResourceFolder].[YourFilename.txt] 68 | /// Model for the template 69 | /// The assembly your resource is in. Defaults to calling assembly. 70 | /// True if Body is HTML (default), false for plain text 71 | /// 72 | public static IFluentEmail UsingBootstrapTemplateFromEmbedded(this IFluentEmail email, string path, T model, 73 | Assembly assembly, bool isHtml = true) 74 | { 75 | email.UsingTemplateFromEmbedded(path, model, assembly, isHtml); 76 | email.CompileBootstrap(isHtml); 77 | return email; 78 | } 79 | 80 | /// 81 | /// Adds Bootstrap template to email from previously configured default embedded resource 82 | /// 83 | /// 84 | /// The IFluentEmail object 85 | /// Path the the embedded resource eg [YourResourceFolder].[YourFilename.txt]. Will be appended to configured root path 86 | /// Model for the template 87 | /// True if Body is HTML (default), false for plain text 88 | /// 89 | public static IFluentEmail UsingBootstrapTemplateFromEmbedded(this IFluentEmail email, string path, T model, 90 | bool isHtml = true) 91 | { 92 | email.UsingTemplateFromEmbedded(path, model, isHtml); 93 | email.CompileBootstrap(isHtml); 94 | return email; 95 | } 96 | 97 | /// 98 | /// Adds Bootstrap template to email from embedded resource 99 | /// 100 | /// 101 | /// The IFluentEmail object 102 | /// The path to the file to load 103 | /// Model for the template 104 | /// True if Body is HTML (default), false for plain text 105 | /// 106 | public static IFluentEmail UsingBootstrapTemplateFromFile(this IFluentEmail email, string filename, T model, 107 | bool isHtml = true) 108 | { 109 | email.UsingTemplateFromFile(filename, model, isHtml); 110 | email.CompileBootstrap(isHtml); 111 | return email; 112 | } 113 | 114 | /// 115 | /// Adds Bootstrap template to email from previously configured default embedded resource 116 | /// 117 | /// 118 | /// The IFluentEmail object 119 | /// The path to the file to load 120 | /// Model for the template 121 | /// The culture of the template (Default is the current culture) 122 | /// True if Body is HTML (default), false for plain text 123 | /// 124 | public static IFluentEmail UsingBootstrapCultureTemplateFromFile(this IFluentEmail email, string filename, 125 | T model, CultureInfo culture, bool isHtml = true) 126 | { 127 | email.UsingCultureTemplateFromFile(filename, model, culture, isHtml); 128 | email.CompileBootstrap(isHtml); 129 | return email; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailgun/MailgunSender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using FluentEmail.Core; 10 | using FluentEmail.Core.Interfaces; 11 | using FluentEmail.Core.Models; 12 | using FluentEmail.Mailgun.HttpHelpers; 13 | using Newtonsoft.Json; 14 | 15 | namespace FluentEmail.Mailgun 16 | { 17 | public class MailgunSender : IMailgunSender 18 | { 19 | private readonly string _apiKey; 20 | private readonly string _domainName; 21 | private HttpClient _httpClient; 22 | 23 | public MailgunSender(string domainName, string apiKey, MailGunRegion mailGunRegion = MailGunRegion.USA) 24 | { 25 | _domainName = domainName; 26 | _apiKey = apiKey; 27 | string url = string.Empty; 28 | switch(mailGunRegion) 29 | { 30 | case MailGunRegion.USA: 31 | url = $"https://api.mailgun.net/v3/{_domainName}/"; 32 | break; 33 | case MailGunRegion.EU: 34 | url = $"https://api.eu.mailgun.net/v3/{_domainName}/"; 35 | break; 36 | 37 | default: 38 | throw new ArgumentException($"'{mailGunRegion}' is not a valid value for {nameof(mailGunRegion)}"); 39 | } 40 | _httpClient = new HttpClient 41 | { 42 | BaseAddress = new Uri(url) 43 | }; 44 | 45 | _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"api:{_apiKey}"))); 46 | } 47 | 48 | public SendResponse Send(IFluentEmail email, CancellationToken? token = null) 49 | { 50 | return SendAsync(email, token).GetAwaiter().GetResult(); 51 | } 52 | 53 | public async Task SendAsync(IFluentEmail email, CancellationToken? token = null) 54 | { 55 | var parameters = BuildMailgunParameters(email); 56 | 57 | parameters.Add(new KeyValuePair(email.Data.IsHtml ? "html" : "text", email.Data.Body)); 58 | 59 | if (!string.IsNullOrEmpty(email.Data.PlaintextAlternativeBody)) 60 | { 61 | parameters.Add(new KeyValuePair("text", email.Data.PlaintextAlternativeBody)); 62 | } 63 | 64 | var files = BuildMailgunFiles(email); 65 | 66 | return await SendAsync(parameters, files, token); 67 | } 68 | 69 | private async Task SendAsync(List> parameters, List files, CancellationToken? token = null) 70 | { 71 | token?.ThrowIfCancellationRequested(); 72 | 73 | var response = await _httpClient.PostMultipart("messages", parameters, files) 74 | .ConfigureAwait(false); 75 | 76 | var result = new SendResponse {MessageId = response.Data?.Id}; 77 | if (!response.Success) 78 | { 79 | result.ErrorMessages.AddRange(response.Errors.Select(x => x.ErrorMessage)); 80 | return result; 81 | } 82 | 83 | return result; 84 | } 85 | 86 | private static List BuildMailgunFiles(IFluentEmail email) 87 | { 88 | var files = new List(); 89 | email.Data.Attachments.ForEach(x => 90 | { 91 | string param; 92 | 93 | if (x.IsInline) 94 | param = "inline"; 95 | else 96 | param = "attachment"; 97 | 98 | files.Add(new HttpFile 99 | { 100 | ParameterName = param, 101 | Data = x.Data, 102 | Filename = x.Filename, 103 | ContentType = x.ContentType 104 | }); 105 | }); 106 | return files; 107 | } 108 | 109 | private static List> BuildMailgunParameters(IFluentEmail email) 110 | { 111 | var parameters = new List>(); 112 | 113 | parameters.Add(new KeyValuePair("from", 114 | $"{email.Data.FromAddress.Name} <{email.Data.FromAddress.EmailAddress}>")); 115 | 116 | email.Data.ToAddresses.ForEach(x => 117 | { 118 | parameters.Add(new KeyValuePair("to", $"{x.Name} <{x.EmailAddress}>")); 119 | }); 120 | email.Data.CcAddresses.ForEach(x => 121 | { 122 | parameters.Add(new KeyValuePair("cc", $"{x.Name} <{x.EmailAddress}>")); 123 | }); 124 | email.Data.BccAddresses.ForEach(x => 125 | { 126 | parameters.Add(new KeyValuePair("bcc", $"{x.Name} <{x.EmailAddress}>")); 127 | }); 128 | email.Data.ReplyToAddresses.ForEach(x => 129 | { 130 | parameters.Add(new KeyValuePair("h:Reply-To", $"{x.Name} <{x.EmailAddress}>")); 131 | }); 132 | parameters.Add(new KeyValuePair("subject", email.Data.Subject)); 133 | 134 | email.Data.Tags.ForEach(x => { parameters.Add(new KeyValuePair("o:tag", x)); }); 135 | 136 | foreach (var emailHeader in email.Data.Headers) 137 | { 138 | var key = emailHeader.Key; 139 | if (!key.StartsWith("h:")) 140 | { 141 | key = "h:" + emailHeader.Key; 142 | } 143 | 144 | parameters.Add(new KeyValuePair(key, emailHeader.Value)); 145 | } 146 | 147 | return parameters; 148 | } 149 | 150 | public async Task SendWithTemplateAsync(IFluentEmail email, string templateName, object templateData, 151 | CancellationToken? token = null) 152 | { 153 | var parameters = BuildMailgunParameters(email); 154 | var files = BuildMailgunFiles(email); 155 | 156 | parameters.Add(new KeyValuePair("template", templateName)); 157 | parameters.Add(new KeyValuePair("h:X-Mailgun-Variables", JsonConvert.SerializeObject(templateData))); 158 | 159 | return await SendAsync(parameters, files, token); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Graph/GraphSender.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core; 2 | using FluentEmail.Core.Interfaces; 3 | using FluentEmail.Core.Models; 4 | using Microsoft.Graph; 5 | using Microsoft.Graph.Auth; 6 | using Microsoft.Identity.Client; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.IO; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace FluentEmail.Graph 14 | { 15 | public class GraphSender : ISender 16 | { 17 | private readonly string _appId; 18 | private readonly string _tenantId; 19 | private readonly string _graphSecret; 20 | private bool _saveSent; 21 | 22 | private ClientCredentialProvider _authProvider; 23 | private GraphServiceClient _graphClient; 24 | private IConfidentialClientApplication _clientApp; 25 | 26 | public GraphSender( 27 | string GraphEmailAppId, 28 | string GraphEmailTenantId, 29 | string GraphEmailSecret, 30 | bool SaveSentItems) 31 | { 32 | _appId = GraphEmailAppId; 33 | _tenantId = GraphEmailTenantId; 34 | _graphSecret = GraphEmailSecret; 35 | _saveSent = SaveSentItems; 36 | 37 | _clientApp = ConfidentialClientApplicationBuilder 38 | .Create(_appId) 39 | .WithTenantId(_tenantId) 40 | .WithClientSecret(_graphSecret) 41 | .Build(); 42 | 43 | _authProvider = new ClientCredentialProvider(_clientApp); 44 | 45 | _graphClient = new GraphServiceClient(_authProvider); 46 | } 47 | 48 | public SendResponse Send(IFluentEmail email, CancellationToken? token = null) 49 | { 50 | return SendAsync(email, token).GetAwaiter().GetResult(); 51 | } 52 | 53 | public async Task SendAsync(IFluentEmail email, CancellationToken? token = null) 54 | { 55 | var message = new Message 56 | { 57 | Subject = email.Data.Subject, 58 | Body = new ItemBody 59 | { 60 | Content = email.Data.Body, 61 | ContentType = email.Data.IsHtml ? BodyType.Html : BodyType.Text 62 | }, 63 | From = new Recipient 64 | { 65 | EmailAddress = new EmailAddress 66 | { 67 | Address = email.Data.FromAddress.EmailAddress, 68 | Name = email.Data.FromAddress.Name 69 | } 70 | } 71 | }; 72 | 73 | if(email.Data.ToAddresses != null && email.Data.ToAddresses.Count > 0) 74 | { 75 | var toRecipients = new List(); 76 | 77 | email.Data.ToAddresses.ForEach(r => toRecipients.Add(new Recipient 78 | { 79 | EmailAddress = new EmailAddress 80 | { 81 | Address = r.EmailAddress.ToString(), 82 | Name = r.Name 83 | } 84 | })); 85 | 86 | message.ToRecipients = toRecipients; 87 | } 88 | 89 | if(email.Data.BccAddresses != null && email.Data.BccAddresses.Count > 0) 90 | { 91 | var bccRecipients = new List(); 92 | 93 | email.Data.BccAddresses.ForEach(r => bccRecipients.Add(new Recipient 94 | { 95 | EmailAddress = new EmailAddress 96 | { 97 | Address = r.EmailAddress.ToString(), 98 | Name = r.Name 99 | } 100 | })); 101 | 102 | message.BccRecipients = bccRecipients; 103 | } 104 | 105 | if (email.Data.CcAddresses != null && email.Data.CcAddresses.Count > 0) 106 | { 107 | var ccRecipients = new List(); 108 | 109 | email.Data.CcAddresses.ForEach(r => ccRecipients.Add(new Recipient 110 | { 111 | EmailAddress = new EmailAddress 112 | { 113 | Address = r.EmailAddress.ToString(), 114 | Name = r.Name 115 | } 116 | })); 117 | 118 | message.CcRecipients = ccRecipients; 119 | } 120 | 121 | if(email.Data.Attachments != null && email.Data.Attachments.Count > 0) 122 | { 123 | message.Attachments = new MessageAttachmentsCollectionPage(); 124 | 125 | email.Data.Attachments.ForEach(a => 126 | { 127 | var attachment = new FileAttachment 128 | { 129 | Name = a.Filename, 130 | ContentType = a.ContentType, 131 | ContentBytes = GetAttachmentBytes(a.Data) 132 | }; 133 | 134 | message.Attachments.Add(attachment); 135 | }); 136 | } 137 | 138 | switch(email.Data.Priority) 139 | { 140 | case Priority.High: 141 | message.Importance = Importance.High; 142 | break; 143 | case Priority.Normal: 144 | message.Importance = Importance.Normal; 145 | break; 146 | case Priority.Low: 147 | message.Importance = Importance.Low; 148 | break; 149 | default: 150 | message.Importance = Importance.Normal; 151 | break; 152 | } 153 | 154 | try 155 | { 156 | await _graphClient.Users[email.Data.FromAddress.EmailAddress] 157 | .SendMail(message, _saveSent) 158 | .Request() 159 | .PostAsync(); 160 | 161 | return new SendResponse 162 | { 163 | MessageId = message.Id 164 | }; 165 | } 166 | catch (Exception ex) 167 | { 168 | return new SendResponse 169 | { 170 | ErrorMessages = new List { ex.Message } 171 | }; 172 | } 173 | } 174 | 175 | private static byte[] GetAttachmentBytes(Stream stream) 176 | { 177 | using(MemoryStream m = new MemoryStream()) 178 | { 179 | stream.CopyTo(m); 180 | return m.ToArray(); 181 | } 182 | } 183 | } 184 | } 185 | --------------------------------------------------------------------------------