├── .gitattributes ├── .github └── workflows │ ├── dotnet-core.yml │ └── publish-packages.yml ├── .gitignore ├── FluentEmail.sln ├── README.markdown ├── assets ├── fluentemail_logo.png └── fluentemail_logo_64x64.png ├── license.txt ├── packages └── repositories.config ├── src ├── Directory.Build.props ├── FluentEmail.Core │ ├── Defaults │ │ ├── ReplaceRenderer.cs │ │ └── SaveToDiskSender.cs │ ├── Email.cs │ ├── EmbeddedResourceHelper.cs │ ├── FluentEmail.Core.csproj │ ├── FluentEmailFactory.cs │ ├── FluentEmailServiceCollectionExtensions.cs │ ├── IFluentEmail.cs │ ├── IFluentEmailFactory.cs │ ├── IHideObjectMembers.cs │ ├── Interfaces │ │ ├── ISender.cs │ │ └── ITemplateRenderer.cs │ ├── ListExtensions.cs │ └── Models │ │ ├── Address.cs │ │ ├── Attachment.cs │ │ ├── EmailData.cs │ │ ├── Priority.cs │ │ └── SendResponse.cs ├── Renderers │ ├── FluentEmail.Liquid │ │ ├── FluentEmail.Liquid.csproj │ │ ├── FluentEmailFluidBuilderExtensions.cs │ │ ├── LiquidParser.cs │ │ ├── LiquidRenderer.cs │ │ └── LiquidRendererOptions.cs │ └── FluentEmail.Razor │ │ ├── FluentEmail.Razor.csproj │ │ ├── FluentEmailRazorBuilderExtensions.cs │ │ ├── IViewBagModel.cs │ │ ├── InMemoryRazorLightProject.cs │ │ └── RazorRenderer.cs └── Senders │ ├── FluentEmail.Exchange │ └── ExchangeSender.cs │ ├── FluentEmail.Graph │ ├── FluentEmail.Graph.csproj │ ├── FluentEmailGraphBuilderExtensions.cs │ └── GraphSender.cs │ ├── FluentEmail.MailKit │ ├── FluentEmail.MailKit.csproj │ ├── FluentEmailMailKitBuilderExtensions.cs │ ├── MailKitSender.cs │ └── SmtpClientOptions.cs │ ├── FluentEmail.Mailgun │ ├── FluentEmail.Mailgun.csproj │ ├── FluentEmailMailgunBuilderExtensions.cs │ ├── HttpHelpers │ │ ├── ApiResponse.cs │ │ ├── BasicAuthHeader.cs │ │ ├── HttpClientHelpers.cs │ │ └── HttpFile.cs │ ├── MailGunRegion.cs │ ├── MailgunResponse.cs │ ├── MailgunSender.cs │ └── README.md │ ├── FluentEmail.Mailtrap │ ├── FluentEmail.Mailtrap.csproj │ ├── FluentEmailMailtrapBuilderExtensions.cs │ └── MailtrapSender.cs │ ├── FluentEmail.SendGrid │ ├── Changelog.md │ ├── FluentEmail.SendGrid.csproj │ ├── FluentEmailSendGridBuilderExtensions.cs │ ├── IFluentEmailExtensions.cs │ ├── ISendGridSender.cs │ └── SendGridSender.cs │ └── FluentEmail.Smtp │ ├── FluentEmail.Smtp.csproj │ ├── FluentEmailSmtpBuilderExtensions.cs │ └── SmtpSender.cs └── test ├── Directory.Build.props ├── FluentEmail.Core.Tests ├── AddressTests.cs ├── AttachmentTests.cs ├── FluentEmail.Core.Tests.csproj ├── FluentEmailTests.cs ├── GraphSenderTests.cs ├── MailKitSmtpSenderTests.cs ├── MailgunSenderTests.cs ├── MailtrapSenderTests.cs ├── SendGridSenderTests.cs ├── SmtpSenderTests.cs ├── TemplateEmailTests.cs ├── logotest.png ├── test-binary.xlsx ├── test-embedded.txt ├── test.he-IL.txt └── test.txt ├── FluentEmail.Liquid.Tests ├── ComplexModel │ ├── ComplexModelRenderTests.cs │ ├── ParentModel.cs │ └── TestData.cs ├── EmailTemplates │ ├── _embedded.liquid │ └── _layout.liquid ├── FluentEmail.Liquid.Tests.csproj └── LiquidTests.cs └── FluentEmail.Razor.Tests ├── FluentEmail.Razor.Tests.csproj ├── RazorTests.cs ├── Shared └── _Layout.cshtml └── _EmbeddedLayout.cshtml /.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 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-core.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Setup .NET Core 15 | uses: actions/setup-dotnet@v1 16 | with: 17 | dotnet-version: 3.1.101 18 | - name: Install dependencies 19 | run: dotnet restore 20 | - name: Build 21 | run: dotnet build --configuration Release --no-restore 22 | - name: Test 23 | run: dotnet test --no-restore --verbosity normal 24 | -------------------------------------------------------------------------------- /.github/workflows/publish-packages.yml: -------------------------------------------------------------------------------- 1 | name: Publish Packages 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Setup .NET Core 14 | uses: actions/setup-dotnet@v1 15 | with: 16 | dotnet-version: 3.1.101 17 | - name: Install dependencies 18 | run: dotnet restore 19 | - name: Build 20 | run: dotnet build --configuration Release --no-restore 21 | 22 | - name: Publish FluentEmail.Core 23 | uses: brandedoutcast/publish-nuget@v2.5.2 24 | with: 25 | PROJECT_FILE_PATH: src/FluentEmail.Core/FluentEmail.Core.csproj 26 | VERSION_FILE_PATH: src/Directory.Build.props 27 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 28 | NUGET_SOURCE: https://api.nuget.org 29 | INCLUDE_SYMBOLS: true 30 | - name: Publish FluentEmail.Smtp 31 | uses: brandedoutcast/publish-nuget@v2.5.2 32 | with: 33 | PROJECT_FILE_PATH: src/Senders/FluentEmail.Smtp/FluentEmail.Smtp.csproj 34 | VERSION_FILE_PATH: src/Directory.Build.props 35 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 36 | NUGET_SOURCE: https://api.nuget.org 37 | INCLUDE_SYMBOLS: true 38 | - name: Publish FluentEmail.Sendgrid 39 | uses: brandedoutcast/publish-nuget@v2.5.2 40 | with: 41 | PROJECT_FILE_PATH: src/Senders/FluentEmail.SendGrid/FluentEmail.SendGrid.csproj 42 | VERSION_FILE_PATH: src/Directory.Build.props 43 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 44 | NUGET_SOURCE: https://api.nuget.org 45 | INCLUDE_SYMBOLS: true 46 | #- name: Publish FluentEmail.MailTrap 47 | # uses: brandedoutcast/publish-nuget@v2.5.2 48 | # with: 49 | # PROJECT_FILE_PATH: src/Senders/FluentEmail.Mailtrap/FluentEmail.Mailtrap.csproj 50 | # NUGET_KEY: ${{secrets.NUGET_API_KEY}} 51 | # NUGET_SOURCE: https://api.nuget.org 52 | - name: Publish FluentEmail.MailKit 53 | uses: brandedoutcast/publish-nuget@v2.5.2 54 | with: 55 | PROJECT_FILE_PATH: src/Senders/FluentEmail.MailKit/FluentEmail.MailKit.csproj 56 | VERSION_FILE_PATH: src/Directory.Build.props 57 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 58 | NUGET_SOURCE: https://api.nuget.org 59 | INCLUDE_SYMBOLS: true 60 | - name: Publish FluentEmail.Mailgun 61 | uses: brandedoutcast/publish-nuget@v2.5.2 62 | with: 63 | PROJECT_FILE_PATH: src/Senders/FluentEmail.Mailgun/FluentEmail.Mailgun.csproj 64 | VERSION_FILE_PATH: src/Directory.Build.props 65 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 66 | NUGET_SOURCE: https://api.nuget.org 67 | INCLUDE_SYMBOLS: true 68 | - name: Publish FluentEmail.Razor 69 | uses: brandedoutcast/publish-nuget@v2.5.2 70 | with: 71 | PROJECT_FILE_PATH: src/Renderers/FluentEmail.Razor/FluentEmail.Razor.csproj 72 | VERSION_FILE_PATH: src/Directory.Build.props 73 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 74 | NUGET_SOURCE: https://api.nuget.org 75 | INCLUDE_SYMBOLS: true 76 | - name: Publish FluentEmail.Liquid 77 | uses: brandedoutcast/publish-nuget@v2.5.2 78 | with: 79 | PROJECT_FILE_PATH: src/Renderers/FluentEmail.Liquid/FluentEmail.Liquid.csproj 80 | VERSION_FILE_PATH: src/Directory.Build.props 81 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 82 | NUGET_SOURCE: https://api.nuget.org 83 | INCLUDE_SYMBOLS: true 84 | # This is currently maintained separately 85 | #- name: Publish FluentEmail.Graph 86 | # uses: brandedoutcast/publish-nuget@v2.5.2 87 | # with: 88 | # PROJECT_FILE_PATH: src/Senders/FluentEmail.Graph/FluentEmail.Graph.csproj 89 | # NUGET_KEY: ${{secrets.NUGET_API_KEY}} 90 | # NUGET_SOURCE: https://api.nuget.org 91 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /FluentEmail.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30011.22 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{6DC215BD-05EF-49A6-ADBE-8AE399952EEC}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D5C44238-0312-43F8-9705-EACFB039A48C}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Renderers", "Renderers", "{12F031E5-8DDC-40A0-9862-8764A6E190C0}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Senders", "Senders", "{926C0980-31D9-4449-903F-3C756044C28A}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7B3C8C77-C54A-4F9E-A241-676AC01E49BB}" 15 | ProjectSection(SolutionItems) = preProject 16 | src\Directory.Build.props = src\Directory.Build.props 17 | README.markdown = README.markdown 18 | EndProjectSection 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Renderers", "Renderers", "{47CB89AC-9615-4FA8-90DE-2D849935C36D}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentEmail.Core", "src\FluentEmail.Core\FluentEmail.Core.csproj", "{386B8AB1-E99A-4E08-83BE-DD1B8C2EE876}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentEmail.Razor", "src\Renderers\FluentEmail.Razor\FluentEmail.Razor.csproj", "{1C444DDD-1E09-4EDB-A6A1-57BA29C54F73}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentEmail.Core.Tests", "test\FluentEmail.Core.Tests\FluentEmail.Core.Tests.csproj", "{72D765E8-81DE-4E18-92C5-ED5BDD65324F}" 27 | EndProject 28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentEmail.Smtp", "src\Senders\FluentEmail.Smtp\FluentEmail.Smtp.csproj", "{3808D62F-9F8A-4644-A965-361968770CFB}" 29 | EndProject 30 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentEmail.Razor.Tests", "test\FluentEmail.Razor.Tests\FluentEmail.Razor.Tests.csproj", "{F54586BA-31F0-4EF4-B965-90007C9AA04C}" 31 | EndProject 32 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentEmail.Mailgun", "src\Senders\FluentEmail.Mailgun\FluentEmail.Mailgun.csproj", "{EFB250E1-92D2-476D-88A5-372B08D73C49}" 33 | EndProject 34 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentEmail.SendGrid", "src\Senders\FluentEmail.SendGrid\FluentEmail.SendGrid.csproj", "{03FFB5B9-54C9-45A3-8E13-3D4B972704F8}" 35 | EndProject 36 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentEmail.Mailtrap", "src\Senders\FluentEmail.Mailtrap\FluentEmail.Mailtrap.csproj", "{FD858CCC-4CF3-4899-B72F-F93C13832174}" 37 | EndProject 38 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentEmail.MailKit", "src\Senders\FluentEmail.MailKit\FluentEmail.MailKit.csproj", "{AEC98399-8C63-4362-B307-0DFB25FFC948}" 39 | EndProject 40 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentEmail.Graph", "src\Senders\FluentEmail.Graph\FluentEmail.Graph.csproj", "{0C7819AD-BC76-465D-9B2A-BE2DA75042F2}" 41 | EndProject 42 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentEmail.Liquid", "src\Renderers\FluentEmail.Liquid\FluentEmail.Liquid.csproj", "{17100F47-A555-4756-A25F-4F05EDAFA74E}" 43 | EndProject 44 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentEmail.Liquid.Tests", "test\FluentEmail.Liquid.Tests\FluentEmail.Liquid.Tests.csproj", "{C8063CBA-D8F3-467A-A75C-63843F0DE862}" 45 | EndProject 46 | Global 47 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 48 | Debug|Any CPU = Debug|Any CPU 49 | Release|Any CPU = Release|Any CPU 50 | EndGlobalSection 51 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 52 | {386B8AB1-E99A-4E08-83BE-DD1B8C2EE876}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {386B8AB1-E99A-4E08-83BE-DD1B8C2EE876}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {386B8AB1-E99A-4E08-83BE-DD1B8C2EE876}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {386B8AB1-E99A-4E08-83BE-DD1B8C2EE876}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {1C444DDD-1E09-4EDB-A6A1-57BA29C54F73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {1C444DDD-1E09-4EDB-A6A1-57BA29C54F73}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {1C444DDD-1E09-4EDB-A6A1-57BA29C54F73}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {1C444DDD-1E09-4EDB-A6A1-57BA29C54F73}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {72D765E8-81DE-4E18-92C5-ED5BDD65324F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {72D765E8-81DE-4E18-92C5-ED5BDD65324F}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {72D765E8-81DE-4E18-92C5-ED5BDD65324F}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {72D765E8-81DE-4E18-92C5-ED5BDD65324F}.Release|Any CPU.Build.0 = Release|Any CPU 64 | {3808D62F-9F8A-4644-A965-361968770CFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 65 | {3808D62F-9F8A-4644-A965-361968770CFB}.Debug|Any CPU.Build.0 = Debug|Any CPU 66 | {3808D62F-9F8A-4644-A965-361968770CFB}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {3808D62F-9F8A-4644-A965-361968770CFB}.Release|Any CPU.Build.0 = Release|Any CPU 68 | {F54586BA-31F0-4EF4-B965-90007C9AA04C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 69 | {F54586BA-31F0-4EF4-B965-90007C9AA04C}.Debug|Any CPU.Build.0 = Debug|Any CPU 70 | {F54586BA-31F0-4EF4-B965-90007C9AA04C}.Release|Any CPU.ActiveCfg = Release|Any CPU 71 | {F54586BA-31F0-4EF4-B965-90007C9AA04C}.Release|Any CPU.Build.0 = Release|Any CPU 72 | {EFB250E1-92D2-476D-88A5-372B08D73C49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 73 | {EFB250E1-92D2-476D-88A5-372B08D73C49}.Debug|Any CPU.Build.0 = Debug|Any CPU 74 | {EFB250E1-92D2-476D-88A5-372B08D73C49}.Release|Any CPU.ActiveCfg = Release|Any CPU 75 | {EFB250E1-92D2-476D-88A5-372B08D73C49}.Release|Any CPU.Build.0 = Release|Any CPU 76 | {03FFB5B9-54C9-45A3-8E13-3D4B972704F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 77 | {03FFB5B9-54C9-45A3-8E13-3D4B972704F8}.Debug|Any CPU.Build.0 = Debug|Any CPU 78 | {03FFB5B9-54C9-45A3-8E13-3D4B972704F8}.Release|Any CPU.ActiveCfg = Release|Any CPU 79 | {03FFB5B9-54C9-45A3-8E13-3D4B972704F8}.Release|Any CPU.Build.0 = Release|Any CPU 80 | {FD858CCC-4CF3-4899-B72F-F93C13832174}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 81 | {FD858CCC-4CF3-4899-B72F-F93C13832174}.Debug|Any CPU.Build.0 = Debug|Any CPU 82 | {FD858CCC-4CF3-4899-B72F-F93C13832174}.Release|Any CPU.ActiveCfg = Release|Any CPU 83 | {FD858CCC-4CF3-4899-B72F-F93C13832174}.Release|Any CPU.Build.0 = Release|Any CPU 84 | {AEC98399-8C63-4362-B307-0DFB25FFC948}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 85 | {AEC98399-8C63-4362-B307-0DFB25FFC948}.Debug|Any CPU.Build.0 = Debug|Any CPU 86 | {AEC98399-8C63-4362-B307-0DFB25FFC948}.Release|Any CPU.ActiveCfg = Release|Any CPU 87 | {AEC98399-8C63-4362-B307-0DFB25FFC948}.Release|Any CPU.Build.0 = Release|Any CPU 88 | {0C7819AD-BC76-465D-9B2A-BE2DA75042F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 89 | {0C7819AD-BC76-465D-9B2A-BE2DA75042F2}.Debug|Any CPU.Build.0 = Debug|Any CPU 90 | {0C7819AD-BC76-465D-9B2A-BE2DA75042F2}.Release|Any CPU.ActiveCfg = Release|Any CPU 91 | {0C7819AD-BC76-465D-9B2A-BE2DA75042F2}.Release|Any CPU.Build.0 = Release|Any CPU 92 | {17100F47-A555-4756-A25F-4F05EDAFA74E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 93 | {17100F47-A555-4756-A25F-4F05EDAFA74E}.Debug|Any CPU.Build.0 = Debug|Any CPU 94 | {17100F47-A555-4756-A25F-4F05EDAFA74E}.Release|Any CPU.ActiveCfg = Release|Any CPU 95 | {17100F47-A555-4756-A25F-4F05EDAFA74E}.Release|Any CPU.Build.0 = Release|Any CPU 96 | {C8063CBA-D8F3-467A-A75C-63843F0DE862}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 97 | {C8063CBA-D8F3-467A-A75C-63843F0DE862}.Debug|Any CPU.Build.0 = Debug|Any CPU 98 | {C8063CBA-D8F3-467A-A75C-63843F0DE862}.Release|Any CPU.ActiveCfg = Release|Any CPU 99 | {C8063CBA-D8F3-467A-A75C-63843F0DE862}.Release|Any CPU.Build.0 = Release|Any CPU 100 | EndGlobalSection 101 | GlobalSection(SolutionProperties) = preSolution 102 | HideSolutionNode = FALSE 103 | EndGlobalSection 104 | GlobalSection(NestedProjects) = preSolution 105 | {12F031E5-8DDC-40A0-9862-8764A6E190C0} = {D5C44238-0312-43F8-9705-EACFB039A48C} 106 | {926C0980-31D9-4449-903F-3C756044C28A} = {D5C44238-0312-43F8-9705-EACFB039A48C} 107 | {47CB89AC-9615-4FA8-90DE-2D849935C36D} = {6DC215BD-05EF-49A6-ADBE-8AE399952EEC} 108 | {386B8AB1-E99A-4E08-83BE-DD1B8C2EE876} = {D5C44238-0312-43F8-9705-EACFB039A48C} 109 | {1C444DDD-1E09-4EDB-A6A1-57BA29C54F73} = {12F031E5-8DDC-40A0-9862-8764A6E190C0} 110 | {72D765E8-81DE-4E18-92C5-ED5BDD65324F} = {6DC215BD-05EF-49A6-ADBE-8AE399952EEC} 111 | {3808D62F-9F8A-4644-A965-361968770CFB} = {926C0980-31D9-4449-903F-3C756044C28A} 112 | {F54586BA-31F0-4EF4-B965-90007C9AA04C} = {47CB89AC-9615-4FA8-90DE-2D849935C36D} 113 | {EFB250E1-92D2-476D-88A5-372B08D73C49} = {926C0980-31D9-4449-903F-3C756044C28A} 114 | {03FFB5B9-54C9-45A3-8E13-3D4B972704F8} = {926C0980-31D9-4449-903F-3C756044C28A} 115 | {FD858CCC-4CF3-4899-B72F-F93C13832174} = {926C0980-31D9-4449-903F-3C756044C28A} 116 | {AEC98399-8C63-4362-B307-0DFB25FFC948} = {926C0980-31D9-4449-903F-3C756044C28A} 117 | {0C7819AD-BC76-465D-9B2A-BE2DA75042F2} = {926C0980-31D9-4449-903F-3C756044C28A} 118 | {17100F47-A555-4756-A25F-4F05EDAFA74E} = {12F031E5-8DDC-40A0-9862-8764A6E190C0} 119 | {C8063CBA-D8F3-467A-A75C-63843F0DE862} = {47CB89AC-9615-4FA8-90DE-2D849935C36D} 120 | EndGlobalSection 121 | GlobalSection(ExtensibilityGlobals) = postSolution 122 | SolutionGuid = {23736554-5288-4B30-9710-B4D9880BCF0B} 123 | EndGlobalSection 124 | GlobalSection(TestCaseManagementSettings) = postSolution 125 | CategoryFile = FluentEmail.vsmdi 126 | EndGlobalSection 127 | EndGlobal 128 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ![alt text](https://github.com/lukencode/FluentEmail/raw/master/assets/fluentemail_logo_64x64.png "FluentEmail") 2 | 3 | # FluentEmail - All in one email sender for .NET and .NET Core 4 | The easiest way to send email from .NET and .NET Core. Use Razor for email templates and send using SendGrid, MailGun, SMTP and more. 5 | 6 | Maintained by Luke Lowrey - follow me on twitter **[@lukencode](https://twitter.com/lukencode)** for updates. See my blog for a detailed guide [A complete guide to send email in .NET](https://lukelowrey.com/dotnet-email-guide-2021/) 7 | 8 | 9 | ## Nuget Packages 10 | 11 | ### Core Library 12 | 13 | * [FluentEmail.Core](src/FluentEmail.Core) - Just the domain model. Includes very basic defaults, but is also included with every other package here. 14 | * [FluentEmail.Smtp](src/Senders/FluentEmail.Smtp) - Send email via SMTP server. 15 | 16 | ### Renderers 17 | 18 | * [FluentEmail.Razor](src/Renderers/FluentEmail.Razor) - 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. 19 | * [FluentEmail.Liquid](src/Renderers/FluentEmail.Liquid) - Generate emails using [Liquid templates](https://shopify.github.io/liquid/). Uses the [Fluid](https://github.com/sebastienros/fluid) project under the hood. 20 | 21 | ### Mail Provider Integrations 22 | 23 | * [FluentEmail.Mailgun](src/Senders/FluentEmail.Mailgun) - Send emails via MailGun's REST API. 24 | * [FluentEmail.SendGrid](src/Senders/FluentEmail.SendGrid) - Send email via the SendGrid API. 25 | * [FluentEmail.Mailtrap](src/Senders/FluentEmail.Mailtrap) - Send emails to Mailtrap. Uses [FluentEmail.Smtp](src/Senders/FluentEmail.Smtp) for delivery. 26 | * [FluentEmail.MailKit](src/Senders/FluentEmail.MailKit) - Send emails using the [MailKit](https://github.com/jstedfast/MailKit) email library. 27 | 28 | ## Basic Usage 29 | ```csharp 30 | var email = await Email 31 | .From("john@email.com") 32 | .To("bob@email.com", "bob") 33 | .Subject("hows it going bob") 34 | .Body("yo bob, long time no see!") 35 | .SendAsync(); 36 | ``` 37 | 38 | 39 | ## Dependency Injection 40 | 41 | Configure FluentEmail in startup.cs with these helper methods. This will inject IFluentEmail (send a single email) and IFluentEmailFactory (used to send multiple emails in a single context) with the 42 | ISender and ITemplateRenderer configured using AddRazorRenderer(), AddSmtpSender() or other packages. 43 | 44 | ```csharp 45 | public void ConfigureServices(IServiceCollection services) 46 | { 47 | services 48 | .AddFluentEmail("fromemail@test.test") 49 | .AddRazorRenderer() 50 | .AddSmtpSender("localhost", 25); 51 | } 52 | ``` 53 | Example to take a dependency on IFluentEmail: 54 | 55 | ```c# 56 | public class EmailService { 57 | 58 | private IFluentEmail _fluentEmail; 59 | 60 | public EmailService(IFluentEmail fluentEmail) { 61 | _fluentEmail = fluentEmail; 62 | } 63 | 64 | public async Task Send() { 65 | await _fluentEmail.To("hellO@gmail.com") 66 | .Body("The body").SendAsync(); 67 | } 68 | } 69 | 70 | ``` 71 | 72 | 73 | 74 | ## Using a Razor template 75 | 76 | ```csharp 77 | // Using Razor templating package (or set using AddRazorRenderer in services) 78 | Email.DefaultRenderer = new RazorRenderer(); 79 | 80 | var template = "Dear @Model.Name, You are totally @Model.Compliment."; 81 | 82 | var email = Email 83 | .From("bob@hotmail.com") 84 | .To("somedude@gmail.com") 85 | .Subject("woo nuget") 86 | .UsingTemplate(template, new { Name = "Luke", Compliment = "Awesome" }); 87 | ``` 88 | 89 | ## Using a Liquid template 90 | 91 | [Liquid templates](https://shopify.github.io/liquid/) are a more secure option for Razor templates as they run in more restricted environment. 92 | While Razor templates have access to whole power of CLR functionality like file access, they also 93 | are more insecure if templates come from untrusted source. Liquid templates also have the benefit of being faster 94 | to parse initially as they don't need heavy compilation step like Razor templates do. 95 | 96 | Model properties are exposed directly as properties in Liquid templates so they also become more compact. 97 | 98 | See [Fluid samples](https://github.com/sebastienros/fluid) for more examples. 99 | 100 | ```csharp 101 | // Using Liquid templating package (or set using AddLiquidRenderer in services) 102 | 103 | // file provider is used to resolve layout files if they are in use 104 | var fileProvider = new PhysicalFileProvider(Path.Combine(someRootPath, "EmailTemplates")); 105 | var options = new LiquidRendererOptions 106 | { 107 | FileProvider = fileProvider 108 | }; 109 | 110 | Email.DefaultRenderer = new LiquidRenderer(Options.Create(options)); 111 | 112 | // template which utilizes layout 113 | var template = @" 114 | {% layout '_layout.liquid' %} 115 | Dear {{ Name }}, You are totally {{ Compliment }}."; 116 | 117 | var email = Email 118 | .From("bob@hotmail.com") 119 | .To("somedude@gmail.com") 120 | .Subject("woo nuget") 121 | .UsingTemplate(template, new ViewModel { Name = "Luke", Compliment = "Awesome" }); 122 | ``` 123 | 124 | ## Sending Emails 125 | 126 | ```csharp 127 | // Using Smtp Sender package (or set using AddSmtpSender in services) 128 | Email.DefaultSender = new SmtpSender(); 129 | 130 | //send normally 131 | email.Send(); 132 | 133 | //send asynchronously 134 | await email.SendAsync(); 135 | ``` 136 | 137 | ## Template File from Disk 138 | 139 | ```csharp 140 | var email = Email 141 | .From("bob@hotmail.com") 142 | .To("somedude@gmail.com") 143 | .Subject("woo nuget") 144 | .UsingTemplateFromFile($"{Directory.GetCurrentDirectory()}/Mytemplate.cshtml", new { Name = "Rad Dude" }); 145 | ``` 146 | 147 | ## Embedded Template File 148 | 149 | **Note for .NET Core 2 users:** You'll need to add the following line to the project containing any embedded razor views. See [this issue for more details](https://github.com/aspnet/Mvc/issues/6021). 150 | 151 | ```xml 152 | false 153 | ``` 154 | 155 | ```csharp 156 | var email = new Email("bob@hotmail.com") 157 | .To("benwholikesbeer@twitter.com") 158 | .Subject("Hey cool name!") 159 | .UsingTemplateFromEmbedded("Example.Project.Namespace.template-name.cshtml", 160 | new { Name = "Bob" }, 161 | TypeFromYourEmbeddedAssembly.GetType().GetTypeInfo().Assembly); 162 | ``` 163 | 164 | -------------------------------------------------------------------------------- /assets/fluentemail_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukencode/FluentEmail/e1379c4d81b7b02cf3e48cf9bd718c307215e4bb/assets/fluentemail_logo.png -------------------------------------------------------------------------------- /assets/fluentemail_logo_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukencode/FluentEmail/e1379c4d81b7b02cf3e48cf9bd718c307215e4bb/assets/fluentemail_logo_64x64.png -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /packages/repositories.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Luke Lowrey;Ben Cull;Github Contributors 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/lukencode/FluentEmail 10 | https://github.com/lukencode/FluentEmail 11 | MIT 12 | 3.0.2 13 | 14 | 15 | true 16 | true 17 | true 18 | true 19 | snupkg 20 | true 21 | 22 | true 23 | true 24 | 25 | true 26 | false 27 | 28 | ../../../assets 29 | 30 | latest 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /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/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/FluentEmail.Core/EmbeddedResourceHelper.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Reflection; 3 | 4 | namespace FluentEmail.Core 5 | { 6 | internal static class EmbeddedResourceHelper 7 | { 8 | internal static string GetResourceAsString(Assembly assembly, string path) 9 | { 10 | string result; 11 | 12 | using (var stream = assembly.GetManifestResourceStream(path)) 13 | using (var reader = new StreamReader(stream)) 14 | { 15 | result = reader.ReadToEnd(); 16 | } 17 | 18 | return result; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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 Fluent Email package here. 5 | Fluent Email 6 | netstandard2.0 7 | ../../assets 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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/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/FluentEmail.Core/IFluentEmail.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Globalization; 3 | using System.Reflection; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using FluentEmail.Core.Interfaces; 7 | using FluentEmail.Core.Models; 8 | 9 | namespace FluentEmail.Core 10 | { 11 | public interface IFluentEmail: IHideObjectMembers 12 | { 13 | EmailData Data { get; set; } 14 | ITemplateRenderer Renderer { get; set; } 15 | ISender Sender { get; set; } 16 | 17 | /// 18 | /// Adds a recipient to the email, Splits name and address on ';' 19 | /// 20 | /// Email address of recipient 21 | /// Name of recipient 22 | /// Instance of the Email class 23 | IFluentEmail To(string emailAddress, string name = null); 24 | 25 | /// 26 | /// Set the send from email address 27 | /// 28 | /// Email address of sender 29 | /// Name of sender 30 | /// Instance of the Email class 31 | IFluentEmail SetFrom(string emailAddress, string name = null); 32 | 33 | /// 34 | /// Adds a recipient to the email 35 | /// 36 | /// Email address of recipient (allows multiple splitting on ';') 37 | /// 38 | IFluentEmail To(string emailAddress); 39 | 40 | /// 41 | /// Adds all recipients in list to email 42 | /// 43 | /// List of recipients 44 | /// Instance of the Email class 45 | IFluentEmail To(IEnumerable
mailAddresses); 46 | 47 | /// 48 | /// Adds a Carbon Copy to the email 49 | /// 50 | /// Email address to cc 51 | /// Name to cc 52 | /// Instance of the Email class 53 | IFluentEmail CC(string emailAddress, string name = ""); 54 | 55 | /// 56 | /// Adds all Carbon Copy in list to an email 57 | /// 58 | /// List of recipients to CC 59 | /// Instance of the Email class 60 | IFluentEmail CC(IEnumerable
mailAddresses); 61 | 62 | /// 63 | /// Adds a blind carbon copy to the email 64 | /// 65 | /// Email address of bcc 66 | /// Name of bcc 67 | /// Instance of the Email class 68 | IFluentEmail BCC(string emailAddress, string name = ""); 69 | 70 | /// 71 | /// Adds all blind carbon copy in list to an email 72 | /// 73 | /// List of recipients to BCC 74 | /// Instance of the Email class 75 | IFluentEmail BCC(IEnumerable
mailAddresses); 76 | 77 | /// 78 | /// Sets the ReplyTo address on the email 79 | /// 80 | /// The ReplyTo Address 81 | /// 82 | IFluentEmail ReplyTo(string address); 83 | 84 | /// 85 | /// Sets the ReplyTo address on the email 86 | /// 87 | /// The ReplyTo Address 88 | /// The Display Name of the ReplyTo 89 | /// 90 | IFluentEmail ReplyTo(string address, string name); 91 | 92 | /// 93 | /// Sets the subject of the email 94 | /// 95 | /// email subject 96 | /// Instance of the Email class 97 | IFluentEmail Subject(string subject); 98 | 99 | /// 100 | /// Adds a Body to the Email 101 | /// 102 | /// The content of the body 103 | /// True if Body is HTML, false for plain text (Optional) 104 | IFluentEmail Body(string body, bool isHtml = false); 105 | 106 | /// 107 | /// Marks the email as High Priority 108 | /// 109 | IFluentEmail HighPriority(); 110 | 111 | /// 112 | /// Marks the email as Low Priority 113 | /// 114 | IFluentEmail LowPriority(); 115 | 116 | /// 117 | /// Set the template rendering engine to use, defaults to RazorEngine 118 | /// 119 | IFluentEmail UsingTemplateEngine(ITemplateRenderer renderer); 120 | 121 | /// 122 | /// Adds template to email from embedded resource 123 | /// 124 | /// 125 | /// Path the the embedded resource eg [YourAssembly].[YourResourceFolder].[YourFilename.txt] 126 | /// Model for the template 127 | /// The assembly your resource is in. Defaults to calling assembly. 128 | /// 129 | IFluentEmail UsingTemplateFromEmbedded(string path, T model, Assembly assembly, bool isHtml = true); 130 | 131 | /// 132 | /// Adds the template file to the email 133 | /// 134 | /// The path to the file to load 135 | /// Model for the template 136 | /// True if Body is HTML, false for plain text (Optional) 137 | /// Instance of the Email class 138 | IFluentEmail UsingTemplateFromFile(string filename, T model, bool isHtml = true); 139 | 140 | /// 141 | /// Adds a culture specific template file to the email 142 | /// 143 | /// The path to the file to load 144 | /// /// The razor model 145 | /// The culture of the template (Default is the current culture) 146 | /// True if Body is HTML, false for plain text (Optional) 147 | /// Instance of the Email class 148 | IFluentEmail UsingCultureTemplateFromFile(string filename, T model, CultureInfo culture, bool isHtml = true); 149 | 150 | /// 151 | /// Adds razor template to the email 152 | /// 153 | /// The razor template 154 | /// Model for the template 155 | /// True if Body is HTML, false for plain text (Optional) 156 | /// Instance of the Email class 157 | IFluentEmail UsingTemplate(string template, T model, bool isHtml = true); 158 | 159 | /// 160 | /// Adds an Attachment to the Email 161 | /// 162 | /// The Attachment to add 163 | /// Instance of the Email class 164 | IFluentEmail Attach(Attachment attachment); 165 | 166 | /// 167 | /// Adds Multiple Attachments to the Email 168 | /// 169 | /// The List of Attachments to add 170 | /// Instance of the Email class 171 | IFluentEmail Attach(IEnumerable attachments); 172 | 173 | /// 174 | /// Sends email synchronously 175 | /// 176 | /// Instance of the Email class 177 | SendResponse Send(CancellationToken? token = null); 178 | 179 | /// 180 | /// Sends email asynchronously 181 | /// 182 | /// 183 | /// 184 | Task SendAsync(CancellationToken? token = null); 185 | 186 | IFluentEmail AttachFromFilename(string filename, string contentType = null, string attachmentName = null); 187 | 188 | /// 189 | /// Adds a Plaintext alternative Body to the Email. Used in conjunction with an HTML email, 190 | /// this allows for email readers without html capability, and also helps avoid spam filters. 191 | /// 192 | /// The content of the body 193 | IFluentEmail PlaintextAlternativeBody(string body); 194 | 195 | /// 196 | /// Adds template to email from embedded resource 197 | /// 198 | /// 199 | /// Path the the embedded resource eg [YourAssembly].[YourResourceFolder].[YourFilename.txt] 200 | /// Model for the template 201 | /// The assembly your resource is in. Defaults to calling assembly. 202 | /// 203 | IFluentEmail PlaintextAlternativeUsingTemplateFromEmbedded(string path, T model, Assembly assembly); 204 | 205 | /// 206 | /// Adds the template file to the email 207 | /// 208 | /// The path to the file to load 209 | /// Model for the template 210 | /// Instance of the Email class 211 | IFluentEmail PlaintextAlternativeUsingTemplateFromFile(string filename, T model); 212 | 213 | /// 214 | /// Adds a culture specific template file to the email 215 | /// 216 | /// The path to the file to load 217 | /// /// The razor model 218 | /// The culture of the template (Default is the current culture) 219 | /// Instance of the Email class 220 | IFluentEmail PlaintextAlternativeUsingCultureTemplateFromFile(string filename, T model, CultureInfo culture); 221 | 222 | /// 223 | /// Adds razor template to the email 224 | /// 225 | /// The razor template 226 | /// Model for the template 227 | /// Instance of the Email class 228 | IFluentEmail PlaintextAlternativeUsingTemplate(string template, T model); 229 | 230 | /// 231 | /// Adds tag to the Email. This is currently only supported by the Mailgun provider. 232 | /// 233 | /// Tag name, max 128 characters, ASCII only 234 | /// Instance of the Email class 235 | IFluentEmail Tag(string tag); 236 | 237 | /// 238 | /// Adds header to the Email. 239 | /// 240 | /// Header name, only printable ASCII allowed. 241 | /// value of the header 242 | /// Instance of the Email class 243 | IFluentEmail Header(string header, string body); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/FluentEmail.Core/IFluentEmailFactory.cs: -------------------------------------------------------------------------------- 1 | namespace FluentEmail.Core 2 | { 3 | public interface IFluentEmailFactory 4 | { 5 | IFluentEmail Create(); 6 | } 7 | } -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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 | netstandard2.0 9 | ../../../assets 10 | NU5104 11 | enable 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Renderers/FluentEmail.Liquid/FluentEmailFluidBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using FluentEmail.Core.Interfaces; 4 | using FluentEmail.Liquid; 5 | 6 | using Microsoft.Extensions.DependencyInjection.Extensions; 7 | 8 | // ReSharper disable once CheckNamespace 9 | namespace Microsoft.Extensions.DependencyInjection 10 | { 11 | public static class FluentEmailFluidBuilderExtensions 12 | { 13 | public static FluentEmailServicesBuilder AddLiquidRenderer( 14 | this FluentEmailServicesBuilder builder, 15 | Action? configure = null) 16 | { 17 | builder.Services.AddOptions(); 18 | if (configure != null) 19 | { 20 | builder.Services.Configure(configure); 21 | } 22 | 23 | builder.Services.TryAddSingleton(); 24 | return builder; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /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/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 | } 23 | 24 | public string Parse(string template, T model, bool isHtml = true) 25 | { 26 | return ParseAsync(template, model, isHtml).GetAwaiter().GetResult(); 27 | } 28 | 29 | public async Task ParseAsync(string template, T model, bool isHtml = true) 30 | { 31 | var rendererOptions = _options.Value; 32 | 33 | // Check for a custom file provider 34 | var fileProvider = rendererOptions.FileProvider; 35 | var viewTemplate = ParseTemplate(template); 36 | 37 | var context = new TemplateContext(model, rendererOptions.TemplateOptions) 38 | { 39 | // provide some services to all statements 40 | AmbientValues = 41 | { 42 | ["FileProvider"] = fileProvider, 43 | ["Sections"] = new Dictionary>() 44 | }, 45 | Options = 46 | { 47 | FileProvider = fileProvider 48 | } 49 | }; 50 | 51 | rendererOptions.ConfigureTemplateContext?.Invoke(context, model!); 52 | 53 | var body = await viewTemplate.RenderAsync(context, rendererOptions.TextEncoder); 54 | 55 | // if a layout is specified while rendering a view, execute it 56 | if (context.AmbientValues.TryGetValue("Layout", out var layoutPath)) 57 | { 58 | context.AmbientValues["Body"] = body; 59 | var layoutTemplate = ParseLiquidFile((string)layoutPath, fileProvider!); 60 | 61 | return await layoutTemplate.RenderAsync(context, rendererOptions.TextEncoder); 62 | } 63 | 64 | return body; 65 | } 66 | 67 | private IFluidTemplate ParseLiquidFile( 68 | string path, 69 | IFileProvider? fileProvider) 70 | { 71 | static void ThrowMissingFileProviderException() 72 | { 73 | const string message = "Cannot parse external file, file provider missing"; 74 | throw new ArgumentNullException(nameof(LiquidRendererOptions.FileProvider), message); 75 | } 76 | 77 | if (fileProvider is null) 78 | { 79 | ThrowMissingFileProviderException(); 80 | } 81 | 82 | var fileInfo = fileProvider!.GetFileInfo(path); 83 | using var stream = fileInfo.CreateReadStream(); 84 | using var sr = new StreamReader(stream); 85 | 86 | return ParseTemplate(sr.ReadToEnd()); 87 | } 88 | 89 | private IFluidTemplate ParseTemplate(string content) 90 | { 91 | if (!_parser.TryParse(content, out var template, out var errors)) 92 | { 93 | throw new Exception(string.Join(Environment.NewLine, errors)); 94 | } 95 | 96 | return template; 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | netstandard2.0 8 | ../../../assets 9 | NU5104 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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/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/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/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.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/Senders/FluentEmail.Graph/FluentEmail.Graph.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Send emails via Microsoft Graph API 5 | Fluent Email - Graph 6 | $(PackageTags);graph 7 | netstandard2.0 8 | NU5104 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | netstandard2.0 9 | FluentEmail.MailKitSmtp 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.MailKit/FluentEmailMailKitBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core.Interfaces; 2 | using FluentEmail.MailKitSmtp; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | 5 | namespace Microsoft.Extensions.DependencyInjection 6 | { 7 | public static class FluentEmailMailKitBuilderExtensions 8 | { 9 | public static FluentEmailServicesBuilder AddMailKitSender(this FluentEmailServicesBuilder builder, SmtpClientOptions smtpClientOptions) 10 | { 11 | builder.Services.TryAdd(ServiceDescriptor.Scoped(_ => new MailKitSender(smtpClientOptions))); 12 | return builder; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.MailKit/MailKitSender.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core; 2 | using FluentEmail.Core.Interfaces; 3 | using FluentEmail.Core.Models; 4 | using MailKit.Net.Smtp; 5 | using MimeKit; 6 | using System; 7 | using System.IO; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace FluentEmail.MailKitSmtp 13 | { 14 | /// 15 | /// Send emails with the MailKit Library. 16 | /// 17 | public class MailKitSender : ISender 18 | { 19 | private readonly SmtpClientOptions _smtpClientOptions; 20 | 21 | /// 22 | /// Creates a sender that uses the given SmtpClientOptions when sending with MailKit. Since the client is internal this will dispose of the client. 23 | /// 24 | /// The SmtpClientOptions to use to create the MailKit client 25 | public MailKitSender(SmtpClientOptions smtpClientOptions) 26 | { 27 | _smtpClientOptions = smtpClientOptions; 28 | } 29 | 30 | /// 31 | /// Send the specified email. 32 | /// 33 | /// A response with any errors and a success boolean. 34 | /// Email. 35 | /// Cancellation Token. 36 | public SendResponse Send(IFluentEmail email, CancellationToken? token = null) 37 | { 38 | var response = new SendResponse(); 39 | var message = CreateMailMessage(email); 40 | 41 | if (token?.IsCancellationRequested ?? false) 42 | { 43 | response.ErrorMessages.Add("Message was cancelled by cancellation token."); 44 | return response; 45 | } 46 | 47 | try 48 | { 49 | if (_smtpClientOptions.UsePickupDirectory) 50 | { 51 | this.SaveToPickupDirectory(message, _smtpClientOptions.MailPickupDirectory).Wait(); 52 | return response; 53 | } 54 | 55 | using (var client = new SmtpClient()) 56 | { 57 | if (_smtpClientOptions.SocketOptions.HasValue) 58 | { 59 | client.Connect( 60 | _smtpClientOptions.Server, 61 | _smtpClientOptions.Port, 62 | _smtpClientOptions.SocketOptions.Value, 63 | token.GetValueOrDefault()); 64 | } 65 | else 66 | { 67 | client.Connect( 68 | _smtpClientOptions.Server, 69 | _smtpClientOptions.Port, 70 | _smtpClientOptions.UseSsl, 71 | token.GetValueOrDefault()); 72 | } 73 | 74 | // Note: only needed if the SMTP server requires authentication 75 | if (_smtpClientOptions.RequiresAuthentication) 76 | { 77 | client.Authenticate(_smtpClientOptions.User, _smtpClientOptions.Password, token.GetValueOrDefault()); 78 | } 79 | 80 | client.Send(message, token.GetValueOrDefault()); 81 | client.Disconnect(true, token.GetValueOrDefault()); 82 | } 83 | } 84 | catch (Exception ex) 85 | { 86 | response.ErrorMessages.Add(ex.Message); 87 | } 88 | 89 | return response; 90 | } 91 | 92 | /// 93 | /// Send the specified email. 94 | /// 95 | /// A response with any errors and a success boolean. 96 | /// Email. 97 | /// Cancellation Token. 98 | public async Task SendAsync(IFluentEmail email, CancellationToken? token = null) 99 | { 100 | var response = new SendResponse(); 101 | var message = CreateMailMessage(email); 102 | 103 | if (token?.IsCancellationRequested ?? false) 104 | { 105 | response.ErrorMessages.Add("Message was cancelled by cancellation token."); 106 | return response; 107 | } 108 | 109 | try 110 | { 111 | if (_smtpClientOptions.UsePickupDirectory) 112 | { 113 | await this.SaveToPickupDirectory(message, _smtpClientOptions.MailPickupDirectory); 114 | return response; 115 | } 116 | 117 | using (var client = new SmtpClient()) 118 | { 119 | if (_smtpClientOptions.SocketOptions.HasValue) 120 | { 121 | await client.ConnectAsync( 122 | _smtpClientOptions.Server, 123 | _smtpClientOptions.Port, 124 | _smtpClientOptions.SocketOptions.Value, 125 | token.GetValueOrDefault()); 126 | } 127 | else 128 | { 129 | await client.ConnectAsync( 130 | _smtpClientOptions.Server, 131 | _smtpClientOptions.Port, 132 | _smtpClientOptions.UseSsl, 133 | token.GetValueOrDefault()); 134 | } 135 | 136 | // Note: only needed if the SMTP server requires authentication 137 | if (_smtpClientOptions.RequiresAuthentication) 138 | { 139 | await client.AuthenticateAsync(_smtpClientOptions.User, _smtpClientOptions.Password, token.GetValueOrDefault()); 140 | } 141 | 142 | await client.SendAsync(message, token.GetValueOrDefault()); 143 | await client.DisconnectAsync(true, token.GetValueOrDefault()); 144 | } 145 | } 146 | catch (Exception ex) 147 | { 148 | response.ErrorMessages.Add(ex.Message); 149 | } 150 | 151 | return response; 152 | } 153 | 154 | /// 155 | /// Saves email to a pickup directory. 156 | /// 157 | /// Message to save for pickup. 158 | /// Pickup directory. 159 | private async Task SaveToPickupDirectory(MimeMessage message, string pickupDirectory) 160 | { 161 | // Note: this will require that you know where the specified pickup directory is. 162 | var path = Path.Combine(pickupDirectory, Guid.NewGuid().ToString() + ".eml"); 163 | 164 | if (File.Exists(path)) 165 | return; 166 | 167 | try 168 | { 169 | using (var stream = new FileStream(path, FileMode.CreateNew)) 170 | { 171 | await message.WriteToAsync(stream); 172 | return; 173 | } 174 | } 175 | catch (IOException) 176 | { 177 | // The file may have been created between our File.Exists() check and 178 | // our attempt to create the stream. 179 | throw; 180 | } 181 | } 182 | 183 | /// 184 | /// Create a MimMessage so MailKit can send it 185 | /// 186 | /// The mail message. 187 | /// Email data. 188 | private MimeMessage CreateMailMessage(IFluentEmail email) 189 | { 190 | var data = email.Data; 191 | 192 | var message = new MimeMessage(); 193 | 194 | // fixes https://github.com/lukencode/FluentEmail/issues/228 195 | // if for any reason, subject header is not added, add it else update it. 196 | if (!message.Headers.Contains(HeaderId.Subject)) 197 | message.Headers.Add(HeaderId.Subject, Encoding.UTF8, data.Subject ?? string.Empty); 198 | else 199 | message.Headers[HeaderId.Subject] = data.Subject ?? string.Empty; 200 | 201 | message.Headers.Add(HeaderId.Encoding, Encoding.UTF8.EncodingName); 202 | 203 | message.From.Add(new MailboxAddress(data.FromAddress.Name, data.FromAddress.EmailAddress)); 204 | 205 | var builder = new BodyBuilder(); 206 | if (!string.IsNullOrEmpty(data.PlaintextAlternativeBody)) 207 | { 208 | builder.TextBody = data.PlaintextAlternativeBody; 209 | builder.HtmlBody = data.Body; 210 | } 211 | else if (!data.IsHtml) 212 | { 213 | builder.TextBody = data.Body; 214 | } 215 | else 216 | { 217 | builder.HtmlBody = data.Body; 218 | } 219 | 220 | data.Attachments.ForEach(x => 221 | { 222 | var attachment = builder.Attachments.Add(x.Filename, x.Data, ContentType.Parse(x.ContentType)); 223 | attachment.ContentId = x.ContentId; 224 | }); 225 | 226 | 227 | message.Body = builder.ToMessageBody(); 228 | 229 | foreach (var header in data.Headers) 230 | { 231 | message.Headers.Add(header.Key, header.Value); 232 | } 233 | 234 | data.ToAddresses.ForEach(x => 235 | { 236 | message.To.Add(new MailboxAddress(x.Name, x.EmailAddress)); 237 | }); 238 | 239 | data.CcAddresses.ForEach(x => 240 | { 241 | message.Cc.Add(new MailboxAddress(x.Name, x.EmailAddress)); 242 | }); 243 | 244 | data.BccAddresses.ForEach(x => 245 | { 246 | message.Bcc.Add(new MailboxAddress(x.Name, x.EmailAddress)); 247 | }); 248 | 249 | data.ReplyToAddresses.ForEach(x => 250 | { 251 | message.ReplyTo.Add(new MailboxAddress(x.Name, x.EmailAddress)); 252 | }); 253 | 254 | switch (data.Priority) 255 | { 256 | case Priority.Low: 257 | message.Priority = MessagePriority.NonUrgent; 258 | break; 259 | case Priority.Normal: 260 | message.Priority = MessagePriority.Normal; 261 | break; 262 | case Priority.High: 263 | message.Priority = MessagePriority.Urgent; 264 | break; 265 | } 266 | 267 | return message; 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.MailKit/SmtpClientOptions.cs: -------------------------------------------------------------------------------- 1 | using MailKit.Security; 2 | 3 | namespace FluentEmail.MailKitSmtp 4 | { 5 | public class SmtpClientOptions 6 | { 7 | public string Server { get; set; } = string.Empty; 8 | public int Port { get; set; } = 25; 9 | public string User { get; set; } = string.Empty; 10 | public string Password { get; set; } = string.Empty; 11 | public bool UseSsl { get; set; } = false; 12 | public bool RequiresAuthentication { get; set; } = false; 13 | public string PreferredEncoding { get; set; } = string.Empty; 14 | public bool UsePickupDirectory { get; set; } = false; 15 | public string MailPickupDirectory { get; set; } = string.Empty; 16 | public SecureSocketOptions? SocketOptions { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /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 | netstandard2.0 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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.Scoped(_ => new MailgunSender(domainName, apiKey, mailGunRegion))); 12 | return builder; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailgun/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 Newtonsoft.Json; 9 | using FluentEmail.Core; 10 | 11 | namespace FluentEmail.Mailgun.HttpHelpers 12 | { 13 | public class HttpClientHelpers 14 | { 15 | public static HttpContent GetPostBody(IEnumerable> parameters) 16 | { 17 | var formatted = parameters.Select(x => x.Key + "=" + x.Value); 18 | return new StringContent(string.Join("&", formatted), Encoding.UTF8, "application/x-www-form-urlencoded"); 19 | } 20 | 21 | public static HttpContent GetJsonBody(object value) 22 | { 23 | return new StringContent(JsonConvert.SerializeObject(value), Encoding.UTF8, "application/json"); 24 | } 25 | 26 | public static HttpContent GetMultipartFormDataContentBody(IEnumerable> parameters, IEnumerable files) 27 | { 28 | var mpContent = new MultipartFormDataContent(); 29 | 30 | parameters.ForEach(p => 31 | { 32 | mpContent.Add(new StringContent(p.Value), p.Key); 33 | }); 34 | 35 | files.ForEach(file => 36 | { 37 | using (var memoryStream = new MemoryStream()) 38 | { 39 | file.Data.CopyTo(memoryStream); 40 | mpContent.Add(new ByteArrayContent(memoryStream.ToArray()), file.ParameterName, file.Filename); 41 | } 42 | }); 43 | 44 | return mpContent; 45 | } 46 | } 47 | 48 | public static class HttpClientExtensions 49 | { 50 | public static async Task> Get(this HttpClient client, string url) 51 | { 52 | var response = await client.GetAsync(url); 53 | var qr = await QuickResponse.FromMessage(response); 54 | return qr.ToApiResponse(); 55 | } 56 | 57 | public static async Task GetFile(this HttpClient client, string url) 58 | { 59 | var response = await client.GetAsync(url); 60 | var qr = await QuickFile.FromMessage(response); 61 | return qr.ToApiResponse(); 62 | } 63 | 64 | public static async Task> Post(this HttpClient client, string url, IEnumerable> parameters) 65 | { 66 | var response = await client.PostAsync(url, HttpClientHelpers.GetPostBody(parameters)); 67 | var qr = await QuickResponse.FromMessage(response); 68 | return qr.ToApiResponse(); 69 | } 70 | 71 | public static async Task> Post(this HttpClient client, string url, object data) 72 | { 73 | var response = await client.PostAsync(url, HttpClientHelpers.GetJsonBody(data)); 74 | var qr = await QuickResponse.FromMessage(response); 75 | return qr.ToApiResponse(); 76 | } 77 | 78 | public static async Task> PostMultipart(this HttpClient client, string url, IEnumerable> parameters, IEnumerable files) 79 | { 80 | var response = await client.PostAsync(url, HttpClientHelpers.GetMultipartFormDataContentBody(parameters, files)).ConfigureAwait(false); 81 | var qr = await QuickResponse.FromMessage(response); 82 | return qr.ToApiResponse(); 83 | } 84 | 85 | public static async Task Delete(this HttpClient client, string url) 86 | { 87 | var response = await client.DeleteAsync(url); 88 | var qr = await QuickResponse.FromMessage(response); 89 | return qr.ToApiResponse(); 90 | } 91 | } 92 | 93 | public class QuickResponse 94 | { 95 | public HttpResponseMessage Message { get; set; } 96 | 97 | public string ResponseBody { get; set; } 98 | 99 | public IList Errors { get; set; } 100 | 101 | public QuickResponse() 102 | { 103 | Errors = new List(); 104 | } 105 | 106 | public ApiResponse ToApiResponse() 107 | { 108 | return new ApiResponse 109 | { 110 | Errors = Errors 111 | }; 112 | } 113 | 114 | public static async Task FromMessage(HttpResponseMessage message) 115 | { 116 | var response = new QuickResponse(); 117 | response.Message = message; 118 | response.ResponseBody = await message.Content.ReadAsStringAsync(); 119 | 120 | if (!message.IsSuccessStatusCode) 121 | { 122 | response.HandleFailedCall(); 123 | } 124 | 125 | return response; 126 | } 127 | 128 | protected void HandleFailedCall() 129 | { 130 | try 131 | { 132 | Errors = JsonConvert.DeserializeObject>(ResponseBody) ?? new List(); 133 | 134 | if (!Errors.Any()) 135 | { 136 | Errors.Add(new ApiError 137 | { 138 | ErrorMessage = !string.IsNullOrEmpty(ResponseBody) ? ResponseBody : Message.StatusCode.ToString() 139 | }); 140 | } 141 | } 142 | catch (Exception) 143 | { 144 | Errors.Add(new ApiError 145 | { 146 | ErrorMessage = !string.IsNullOrEmpty(ResponseBody) ? ResponseBody : Message.StatusCode.ToString() 147 | }); 148 | } 149 | } 150 | } 151 | 152 | public class QuickResponse : QuickResponse 153 | { 154 | public T Data { get; set; } 155 | 156 | public new ApiResponse ToApiResponse() 157 | { 158 | return new ApiResponse 159 | { 160 | Errors = Errors, 161 | Data = Data 162 | }; 163 | } 164 | 165 | public new static async Task> FromMessage(HttpResponseMessage message) 166 | { 167 | var response = new QuickResponse(); 168 | response.Message = message; 169 | response.ResponseBody = await message.Content.ReadAsStringAsync(); 170 | 171 | if (message.IsSuccessStatusCode) 172 | { 173 | try 174 | { 175 | response.Data = JsonConvert.DeserializeObject(response.ResponseBody); 176 | } 177 | catch (Exception) 178 | { 179 | response.HandleFailedCall(); 180 | } 181 | } 182 | else 183 | { 184 | response.HandleFailedCall(); 185 | } 186 | 187 | return response; 188 | } 189 | } 190 | 191 | public class QuickFile : QuickResponse 192 | { 193 | public new static async Task FromMessage(HttpResponseMessage message) 194 | { 195 | var response = new QuickFile(); 196 | response.Message = message; 197 | response.ResponseBody = await message.Content.ReadAsStringAsync(); 198 | 199 | if (message.IsSuccessStatusCode) 200 | { 201 | response.Data = await message.Content.ReadAsStreamAsync(); 202 | } 203 | else 204 | { 205 | response.HandleFailedCall(); 206 | } 207 | 208 | return response; 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /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.Mailgun/MailGunRegion.cs: -------------------------------------------------------------------------------- 1 | namespace FluentEmail.Mailgun 2 | { 3 | public enum MailGunRegion 4 | { 5 | USA, 6 | EU 7 | } 8 | } -------------------------------------------------------------------------------- /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.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 | 14 | namespace FluentEmail.Mailgun 15 | { 16 | public class MailgunSender : ISender 17 | { 18 | private readonly string _apiKey; 19 | private readonly string _domainName; 20 | private HttpClient _httpClient; 21 | 22 | public MailgunSender(string domainName, string apiKey, MailGunRegion mailGunRegion = MailGunRegion.USA) 23 | { 24 | _domainName = domainName; 25 | _apiKey = apiKey; 26 | string url = string.Empty; 27 | switch(mailGunRegion) 28 | { 29 | case MailGunRegion.USA: 30 | url = $"https://api.mailgun.net/v3/{_domainName}/"; 31 | break; 32 | case MailGunRegion.EU: 33 | url = $"https://api.eu.mailgun.net/v3/{_domainName}/"; 34 | break; 35 | 36 | default: 37 | throw new ArgumentException($"'{mailGunRegion}' is not a valid value for {nameof(mailGunRegion)}"); 38 | } 39 | _httpClient = new HttpClient 40 | { 41 | BaseAddress = new Uri(url) 42 | }; 43 | 44 | _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"api:{_apiKey}"))); 45 | } 46 | 47 | public SendResponse Send(IFluentEmail email, CancellationToken? token = null) 48 | { 49 | return SendAsync(email, token).GetAwaiter().GetResult(); 50 | } 51 | 52 | public async Task SendAsync(IFluentEmail email, CancellationToken? token = null) 53 | { 54 | var parameters = new List>(); 55 | 56 | parameters.Add(new KeyValuePair("from", $"{email.Data.FromAddress.Name} <{email.Data.FromAddress.EmailAddress}>")); 57 | email.Data.ToAddresses.ForEach(x => { 58 | parameters.Add(new KeyValuePair("to", $"{x.Name} <{x.EmailAddress}>")); 59 | }); 60 | email.Data.CcAddresses.ForEach(x => { 61 | parameters.Add(new KeyValuePair("cc", $"{x.Name} <{x.EmailAddress}>")); 62 | }); 63 | email.Data.BccAddresses.ForEach(x => { 64 | parameters.Add(new KeyValuePair("bcc", $"{x.Name} <{x.EmailAddress}>")); 65 | }); 66 | email.Data.ReplyToAddresses.ForEach(x => { 67 | parameters.Add(new KeyValuePair("h:Reply-To", $"{x.Name} <{x.EmailAddress}>")); 68 | }); 69 | parameters.Add(new KeyValuePair("subject", email.Data.Subject)); 70 | 71 | parameters.Add(new KeyValuePair(email.Data.IsHtml ? "html" : "text", email.Data.Body)); 72 | 73 | if (!string.IsNullOrEmpty(email.Data.PlaintextAlternativeBody)) 74 | { 75 | parameters.Add(new KeyValuePair("text", email.Data.PlaintextAlternativeBody)); 76 | } 77 | 78 | email.Data.Tags.ForEach(x => 79 | { 80 | parameters.Add(new KeyValuePair("o:tag", x)); 81 | }); 82 | 83 | foreach (var emailHeader in email.Data.Headers) 84 | { 85 | var key = emailHeader.Key; 86 | if (!key.StartsWith("h:")) 87 | { 88 | key = "h:" + emailHeader.Key; 89 | } 90 | 91 | parameters.Add(new KeyValuePair(key, emailHeader.Value)); 92 | } 93 | 94 | var files = new List(); 95 | email.Data.Attachments.ForEach(x => 96 | { 97 | string param; 98 | 99 | if (x.IsInline) 100 | param = "inline"; 101 | else 102 | param = "attachment"; 103 | 104 | files.Add(new HttpFile 105 | { 106 | ParameterName = param, 107 | Data = x.Data, 108 | Filename = x.Filename, 109 | ContentType = x.ContentType 110 | }); 111 | }); 112 | 113 | var response = await _httpClient.PostMultipart("messages", parameters, files).ConfigureAwait(false); 114 | 115 | var result = new SendResponse {MessageId = response.Data?.Id}; 116 | if (!response.Success) 117 | { 118 | result.ErrorMessages.AddRange(response.Errors.Select(x => x.ErrorMessage)); 119 | return result; 120 | } 121 | 122 | return result; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Senders/FluentEmail.Mailgun/README.md: -------------------------------------------------------------------------------- 1 | # Fluent Email - MailGun 2 | 3 | Send email via the MailGun REST API. 4 | 5 | ## Packages 6 | 7 | `FluentEmail.Mailgun` 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 | 40 | If you'd like to create your own sender for another service, check out the source code. All you need to do is implement the `ISender` interface :) 41 | -------------------------------------------------------------------------------- /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 | netstandard2.0 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /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.Mailtrap/MailtrapSender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Net.Mail; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using FluentEmail.Core; 8 | using FluentEmail.Core.Interfaces; 9 | using FluentEmail.Core.Models; 10 | using FluentEmail.Smtp; 11 | 12 | namespace FluentEmail.Mailtrap 13 | { 14 | /// 15 | /// Send emails to a Mailtrap.io inbox 16 | /// 17 | public class MailtrapSender : ISender, IDisposable 18 | { 19 | private readonly SmtpClient _smtpClient; 20 | private static readonly int[] ValidPorts = {25, 465, 2525}; 21 | 22 | /// 23 | /// Creates a sender that uses the given Mailtrap credentials, but does not dispose it. 24 | /// 25 | /// Username of your mailtrap.io SMTP inbox 26 | /// Password of your mailtrap.io SMTP inbox 27 | /// Host address for the Mailtrap.io SMTP inbox 28 | /// Port for the Mailtrap.io SMTP server. Accepted values are 25, 465 or 2525. 29 | public MailtrapSender(string userName, string password, string host = "smtp.mailtrap.io", int? port = null) 30 | { 31 | if (string.IsNullOrWhiteSpace(userName)) 32 | throw new ArgumentException("Mailtrap UserName needs to be supplied", nameof(userName)); 33 | 34 | if (string.IsNullOrWhiteSpace(password)) 35 | throw new ArgumentException("Mailtrap Password needs to be supplied", nameof(password)); 36 | 37 | if (port.HasValue && !ValidPorts.Contains(port.Value)) 38 | throw new ArgumentException("Mailtrap Port needs to be either 25, 465 or 2525", nameof(port)); 39 | 40 | _smtpClient = new SmtpClient(host, port.GetValueOrDefault(2525)) 41 | { 42 | Credentials = new NetworkCredential(userName, password), 43 | EnableSsl = true 44 | }; 45 | } 46 | 47 | public void Dispose() => _smtpClient?.Dispose(); 48 | 49 | public SendResponse Send(IFluentEmail email, CancellationToken? token = null) 50 | { 51 | var smtpSender = new SmtpSender(_smtpClient); 52 | return smtpSender.Send(email, token); 53 | } 54 | 55 | public Task SendAsync(IFluentEmail email, CancellationToken? token = null) 56 | { 57 | var smtpSender = new SmtpSender(_smtpClient); 58 | return smtpSender.SendAsync(email, token); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /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/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 | netstandard2.0 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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.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/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.SendGrid/SendGridSender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using FluentEmail.Core; 8 | using FluentEmail.Core.Interfaces; 9 | using FluentEmail.Core.Models; 10 | using SendGrid; 11 | using SendGrid.Helpers.Mail; 12 | using SendGridAttachment = SendGrid.Helpers.Mail.Attachment; 13 | 14 | namespace FluentEmail.SendGrid 15 | { 16 | public class SendGridSender : ISendGridSender 17 | { 18 | private readonly string _apiKey; 19 | private readonly bool _sandBoxMode; 20 | 21 | public SendGridSender(string apiKey, bool sandBoxMode = false) 22 | { 23 | _apiKey = apiKey; 24 | _sandBoxMode = sandBoxMode; 25 | } 26 | public SendResponse Send(IFluentEmail email, CancellationToken? token = null) 27 | { 28 | return SendAsync(email, token).GetAwaiter().GetResult(); 29 | } 30 | 31 | public async Task SendAsync(IFluentEmail email, CancellationToken? token = null) 32 | { 33 | var mailMessage = await BuildSendGridMessage(email); 34 | 35 | if (email.Data.IsHtml) 36 | { 37 | mailMessage.HtmlContent = email.Data.Body; 38 | } 39 | else 40 | { 41 | mailMessage.PlainTextContent = email.Data.Body; 42 | } 43 | 44 | if (!string.IsNullOrEmpty(email.Data.PlaintextAlternativeBody)) 45 | { 46 | mailMessage.PlainTextContent = email.Data.PlaintextAlternativeBody; 47 | } 48 | 49 | var sendResponse = await SendViaSendGrid(mailMessage, token); 50 | 51 | return sendResponse; 52 | } 53 | 54 | public async Task SendWithTemplateAsync(IFluentEmail email, string templateId, object templateData, CancellationToken? token = null) 55 | { 56 | var mailMessage = await BuildSendGridMessage(email); 57 | 58 | mailMessage.SetTemplateId(templateId); 59 | mailMessage.SetTemplateData(templateData); 60 | 61 | var sendResponse = await SendViaSendGrid(mailMessage, token); 62 | 63 | return sendResponse; 64 | } 65 | 66 | private async Task BuildSendGridMessage(IFluentEmail email) 67 | { 68 | var mailMessage = new SendGridMessage(); 69 | mailMessage.SetSandBoxMode(_sandBoxMode); 70 | 71 | mailMessage.SetFrom(ConvertAddress(email.Data.FromAddress)); 72 | 73 | if (email.Data.ToAddresses.Any(a => !string.IsNullOrWhiteSpace(a.EmailAddress))) 74 | mailMessage.AddTos(email.Data.ToAddresses.Select(ConvertAddress).ToList()); 75 | 76 | if (email.Data.CcAddresses.Any(a => !string.IsNullOrWhiteSpace(a.EmailAddress))) 77 | mailMessage.AddCcs(email.Data.CcAddresses.Select(ConvertAddress).ToList()); 78 | 79 | if (email.Data.BccAddresses.Any(a => !string.IsNullOrWhiteSpace(a.EmailAddress))) 80 | mailMessage.AddBccs(email.Data.BccAddresses.Select(ConvertAddress).ToList()); 81 | 82 | if (email.Data.ReplyToAddresses.Any(a => !string.IsNullOrWhiteSpace(a.EmailAddress))) 83 | // SendGrid does not support multiple ReplyTo addresses 84 | mailMessage.SetReplyTo(email.Data.ReplyToAddresses.Select(ConvertAddress).First()); 85 | 86 | mailMessage.SetSubject(email.Data.Subject); 87 | 88 | if (email.Data.Headers.Any()) 89 | { 90 | mailMessage.AddHeaders(email.Data.Headers.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); 91 | } 92 | 93 | if(email.Data.Tags != null && email.Data.Tags.Any()) 94 | { 95 | mailMessage.Categories = email.Data.Tags.ToList(); 96 | } 97 | 98 | if (email.Data.IsHtml) 99 | { 100 | mailMessage.HtmlContent = email.Data.Body; 101 | } 102 | else 103 | { 104 | mailMessage.PlainTextContent = email.Data.Body; 105 | } 106 | 107 | switch (email.Data.Priority) 108 | { 109 | case Priority.High: 110 | // https://stackoverflow.com/questions/23230250/set-email-priority-with-sendgrid-api 111 | mailMessage.AddHeader("Priority", "Urgent"); 112 | mailMessage.AddHeader("Importance", "High"); 113 | // https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmail/2bb19f1b-b35e-4966-b1cb-1afd044e83ab 114 | mailMessage.AddHeader("X-Priority", "1"); 115 | mailMessage.AddHeader("X-MSMail-Priority", "High"); 116 | break; 117 | 118 | case Priority.Normal: 119 | // Do not set anything. 120 | // Leave default values. It means Normal Priority. 121 | break; 122 | 123 | case Priority.Low: 124 | // https://stackoverflow.com/questions/23230250/set-email-priority-with-sendgrid-api 125 | mailMessage.AddHeader("Priority", "Non-Urgent"); 126 | mailMessage.AddHeader("Importance", "Low"); 127 | // https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmail/2bb19f1b-b35e-4966-b1cb-1afd044e83ab 128 | mailMessage.AddHeader("X-Priority", "5"); 129 | mailMessage.AddHeader("X-MSMail-Priority", "Low"); 130 | break; 131 | } 132 | 133 | if (email.Data.Attachments.Any()) 134 | { 135 | foreach (var attachment in email.Data.Attachments) 136 | { 137 | var sendGridAttachment = await ConvertAttachment(attachment); 138 | mailMessage.AddAttachment(sendGridAttachment.Filename, sendGridAttachment.Content, 139 | sendGridAttachment.Type, sendGridAttachment.Disposition, sendGridAttachment.ContentId); 140 | } 141 | } 142 | 143 | return mailMessage; 144 | } 145 | 146 | private async Task SendViaSendGrid(SendGridMessage mailMessage, CancellationToken? token = null) 147 | { 148 | var sendGridClient = new SendGridClient(_apiKey); 149 | var sendGridResponse = await sendGridClient.SendEmailAsync(mailMessage, token.GetValueOrDefault()); 150 | 151 | var sendResponse = new SendResponse(); 152 | 153 | if (sendGridResponse.Headers.TryGetValues( 154 | "X-Message-ID", 155 | out IEnumerable messageIds)) 156 | { 157 | sendResponse.MessageId = messageIds.FirstOrDefault(); 158 | } 159 | 160 | if (IsHttpSuccess((int)sendGridResponse.StatusCode)) return sendResponse; 161 | 162 | sendResponse.ErrorMessages.Add($"{sendGridResponse.StatusCode}"); 163 | var messageBodyDictionary = await sendGridResponse.DeserializeResponseBodyAsync(); 164 | 165 | if (messageBodyDictionary.ContainsKey("errors")) 166 | { 167 | var errors = messageBodyDictionary["errors"]; 168 | 169 | foreach (var error in errors) 170 | { 171 | sendResponse.ErrorMessages.Add($"{error}"); 172 | } 173 | } 174 | 175 | return sendResponse; 176 | } 177 | 178 | private EmailAddress ConvertAddress(Address address) => new EmailAddress(address.EmailAddress, address.Name); 179 | 180 | private async Task ConvertAttachment(Core.Models.Attachment attachment) => new SendGridAttachment 181 | { 182 | Content = await GetAttachmentBase64String(attachment.Data), 183 | Filename = attachment.Filename, 184 | Type = attachment.ContentType 185 | }; 186 | 187 | private async Task GetAttachmentBase64String(Stream stream) 188 | { 189 | using (var ms = new MemoryStream()) 190 | { 191 | await stream.CopyToAsync(ms); 192 | return Convert.ToBase64String(ms.ToArray()); 193 | } 194 | } 195 | 196 | private bool IsHttpSuccess(int statusCode) 197 | { 198 | return statusCode >= 200 && statusCode < 300; 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /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 | netstandard2.0 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /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/Senders/FluentEmail.Smtp/SmtpSender.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core; 2 | using FluentEmail.Core.Interfaces; 3 | using FluentEmail.Core.Models; 4 | using System; 5 | using System.Net.Mail; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace FluentEmail.Smtp 11 | { 12 | public class SmtpSender : ISender 13 | { 14 | private readonly Func _clientFactory; 15 | private readonly SmtpClient _smtpClient; 16 | 17 | /// 18 | /// Creates a sender using the default SMTP settings. 19 | /// 20 | public SmtpSender() : this(() => new SmtpClient()) 21 | { 22 | } 23 | 24 | /// 25 | /// Creates a sender that uses the factory to create and dispose an SmtpClient with each email sent. 26 | /// 27 | /// 28 | public SmtpSender(Func clientFactory) 29 | { 30 | _clientFactory = clientFactory; 31 | } 32 | 33 | /// 34 | /// Creates a sender that uses the given SmtpClient, but does not dispose it. 35 | /// 36 | /// 37 | public SmtpSender(SmtpClient smtpClient) 38 | { 39 | _smtpClient = smtpClient; 40 | } 41 | 42 | public SendResponse Send(IFluentEmail email, CancellationToken? token = null) 43 | { 44 | // Uses task.run to negate Synchronisation Context 45 | // see: https://stackoverflow.com/questions/28333396/smtpclient-sendmailasync-causes-deadlock-when-throwing-a-specific-exception/28445791#28445791 46 | return Task.Run(() => SendAsync(email, token)).Result; 47 | } 48 | 49 | public async Task SendAsync(IFluentEmail email, CancellationToken? token = null) 50 | { 51 | var response = new SendResponse(); 52 | 53 | var message = CreateMailMessage(email); 54 | 55 | if (token?.IsCancellationRequested ?? false) 56 | { 57 | response.ErrorMessages.Add("Message was cancelled by cancellation token."); 58 | return response; 59 | } 60 | 61 | if (_smtpClient == null) 62 | { 63 | using (var client = _clientFactory()) 64 | { 65 | await client.SendMailExAsync(message, token ?? default); 66 | } 67 | } 68 | else 69 | { 70 | await _smtpClient.SendMailExAsync(message, token ?? default); 71 | } 72 | 73 | return response; 74 | } 75 | 76 | private MailMessage CreateMailMessage(IFluentEmail email) 77 | { 78 | var data = email.Data; 79 | MailMessage message = null; 80 | 81 | // Smtp seems to require the HTML version as the alternative. 82 | if (!string.IsNullOrEmpty(data.PlaintextAlternativeBody)) 83 | { 84 | message = new MailMessage 85 | { 86 | Subject = data.Subject, 87 | Body = data.PlaintextAlternativeBody, 88 | IsBodyHtml = false, 89 | From = new MailAddress(data.FromAddress.EmailAddress, data.FromAddress.Name) 90 | }; 91 | 92 | var mimeType = new System.Net.Mime.ContentType("text/html; charset=UTF-8"); 93 | AlternateView alternate = AlternateView.CreateAlternateViewFromString(data.Body, mimeType); 94 | message.AlternateViews.Add(alternate); 95 | } 96 | else 97 | { 98 | message = new MailMessage 99 | { 100 | Subject = data.Subject, 101 | Body = data.Body, 102 | IsBodyHtml = data.IsHtml, 103 | BodyEncoding = Encoding.UTF8, 104 | SubjectEncoding = Encoding.UTF8, 105 | From = new MailAddress(data.FromAddress.EmailAddress, data.FromAddress.Name) 106 | }; 107 | } 108 | 109 | foreach (var header in data.Headers) 110 | { 111 | message.Headers.Add(header.Key, header.Value); 112 | } 113 | 114 | data.ToAddresses.ForEach(x => 115 | { 116 | message.To.Add(new MailAddress(x.EmailAddress, x.Name)); 117 | }); 118 | 119 | data.CcAddresses.ForEach(x => 120 | { 121 | message.CC.Add(new MailAddress(x.EmailAddress, x.Name)); 122 | }); 123 | 124 | data.BccAddresses.ForEach(x => 125 | { 126 | message.Bcc.Add(new MailAddress(x.EmailAddress, x.Name)); 127 | }); 128 | 129 | data.ReplyToAddresses.ForEach(x => 130 | { 131 | message.ReplyToList.Add(new MailAddress(x.EmailAddress, x.Name)); 132 | }); 133 | 134 | switch (data.Priority) 135 | { 136 | case Priority.Low: 137 | message.Priority = MailPriority.Low; 138 | break; 139 | case Priority.Normal: 140 | message.Priority = MailPriority.Normal; 141 | break; 142 | case Priority.High: 143 | message.Priority = MailPriority.High; 144 | break; 145 | } 146 | 147 | data.Attachments.ForEach(x => 148 | { 149 | System.Net.Mail.Attachment a = new System.Net.Mail.Attachment(x.Data, x.Filename, x.ContentType); 150 | 151 | a.ContentId = x.ContentId; 152 | 153 | message.Attachments.Add(a); 154 | }); 155 | 156 | return message; 157 | } 158 | } 159 | 160 | // Taken from https://stackoverflow.com/questions/28333396/smtpclient-sendmailasync-causes-deadlock-when-throwing-a-specific-exception/28445791#28445791 161 | // SmtpClient causes deadlock when throwing exceptions. This fixes that. 162 | public static class SendMailEx 163 | { 164 | public static Task SendMailExAsync( 165 | this SmtpClient @this, 166 | MailMessage message, 167 | CancellationToken token = default(CancellationToken)) 168 | { 169 | // use Task.Run to negate SynchronizationContext 170 | return Task.Run(() => SendMailExImplAsync(@this, message, token)); 171 | } 172 | 173 | private static async Task SendMailExImplAsync( 174 | SmtpClient client, 175 | MailMessage message, 176 | CancellationToken token) 177 | { 178 | token.ThrowIfCancellationRequested(); 179 | 180 | var tcs = new TaskCompletionSource(); 181 | SendCompletedEventHandler handler = null; 182 | Action unsubscribe = () => client.SendCompleted -= handler; 183 | 184 | handler = async (_, e) => 185 | { 186 | unsubscribe(); 187 | 188 | // a hack to complete the handler asynchronously 189 | await Task.Yield(); 190 | 191 | if (e.UserState != tcs) 192 | tcs.TrySetException(new InvalidOperationException("Unexpected UserState")); 193 | else if (e.Cancelled) 194 | tcs.TrySetCanceled(); 195 | else if (e.Error != null) 196 | tcs.TrySetException(e.Error); 197 | else 198 | tcs.TrySetResult(true); 199 | }; 200 | 201 | client.SendCompleted += handler; 202 | try 203 | { 204 | client.SendAsync(message, tcs); 205 | using (token.Register(() => 206 | { 207 | client.SendAsyncCancel(); 208 | }, useSynchronizationContext: false)) 209 | { 210 | await tcs.Task; 211 | } 212 | } 213 | finally 214 | { 215 | unsubscribe(); 216 | } 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /test/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/AddressTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace FluentEmail.Core.Tests 4 | { 5 | [TestFixture] 6 | public class AddressTests 7 | { 8 | [Test] 9 | public void SplitAddress_Test() 10 | { 11 | var email = Email 12 | .From("test@test.com") 13 | .To("james@test.com;john@test.com", "James 1;John 2"); 14 | 15 | Assert.AreEqual(2, email.Data.ToAddresses.Count); 16 | Assert.AreEqual("james@test.com", email.Data.ToAddresses[0].EmailAddress); 17 | Assert.AreEqual("john@test.com", email.Data.ToAddresses[1].EmailAddress); 18 | Assert.AreEqual("James 1", email.Data.ToAddresses[0].Name); 19 | Assert.AreEqual("John 2", email.Data.ToAddresses[1].Name); 20 | } 21 | 22 | [Test] 23 | public void SplitAddress_Test2() 24 | { 25 | var email = Email 26 | .From("test@test.com") 27 | .To("james@test.com; john@test.com", "James 1"); 28 | 29 | Assert.AreEqual(2, email.Data.ToAddresses.Count); 30 | Assert.AreEqual("james@test.com", email.Data.ToAddresses[0].EmailAddress); 31 | Assert.AreEqual("john@test.com", email.Data.ToAddresses[1].EmailAddress); 32 | Assert.AreEqual("James 1", email.Data.ToAddresses[0].Name); 33 | Assert.AreEqual(string.Empty, email.Data.ToAddresses[1].Name); 34 | } 35 | 36 | [Test] 37 | public void SplitAddress_Test3() 38 | { 39 | var email = Email 40 | .From("test@test.com") 41 | .To("james@test.com; john@test.com; Fred@test.com", "James 1;;Fred"); 42 | 43 | Assert.AreEqual(3, email.Data.ToAddresses.Count); 44 | Assert.AreEqual("james@test.com", email.Data.ToAddresses[0].EmailAddress); 45 | Assert.AreEqual("john@test.com", email.Data.ToAddresses[1].EmailAddress); 46 | Assert.AreEqual("Fred@test.com", email.Data.ToAddresses[2].EmailAddress); 47 | Assert.AreEqual("James 1", email.Data.ToAddresses[0].Name); 48 | Assert.AreEqual(string.Empty, email.Data.ToAddresses[1].Name); 49 | Assert.AreEqual("Fred", email.Data.ToAddresses[2].Name); 50 | } 51 | 52 | [Test] 53 | public void SetFromAddress() 54 | { 55 | var email = new Email(); 56 | email.SetFrom("test@test.test", "test"); 57 | 58 | Assert.AreEqual("test@test.test", email.Data.FromAddress.EmailAddress); 59 | Assert.AreEqual("test", email.Data.FromAddress.Name); 60 | } 61 | 62 | #region Refactored tests using setup through constructor. 63 | [Test] 64 | public void New_SplitAddress_Test() 65 | { 66 | var email = new Email() 67 | .To("james@test.com;john@test.com", "James 1;John 2"); 68 | 69 | Assert.AreEqual(2, email.Data.ToAddresses.Count); 70 | Assert.AreEqual("james@test.com", email.Data.ToAddresses[0].EmailAddress); 71 | Assert.AreEqual("john@test.com", email.Data.ToAddresses[1].EmailAddress); 72 | Assert.AreEqual("James 1", email.Data.ToAddresses[0].Name); 73 | Assert.AreEqual("John 2", email.Data.ToAddresses[1].Name); 74 | } 75 | 76 | 77 | [Test] 78 | public void New_SplitAddress_Test2() 79 | { 80 | var email = new Email() 81 | .To("james@test.com; john@test.com", "James 1"); 82 | 83 | Assert.AreEqual(2, email.Data.ToAddresses.Count); 84 | Assert.AreEqual("james@test.com", email.Data.ToAddresses[0].EmailAddress); 85 | Assert.AreEqual("john@test.com", email.Data.ToAddresses[1].EmailAddress); 86 | Assert.AreEqual("James 1", email.Data.ToAddresses[0].Name); 87 | Assert.AreEqual(string.Empty, email.Data.ToAddresses[1].Name); 88 | } 89 | 90 | 91 | public void New_SplitAddress_Test3() 92 | { 93 | var email = new Email() 94 | .To("james@test.com; john@test.com; Fred@test.com", "James 1;;Fred"); 95 | 96 | Assert.AreEqual(3, email.Data.ToAddresses.Count); 97 | Assert.AreEqual("james@test.com", email.Data.ToAddresses[0].EmailAddress); 98 | Assert.AreEqual("john@test.com", email.Data.ToAddresses[1].EmailAddress); 99 | Assert.AreEqual("Fred@test.com", email.Data.ToAddresses[2].EmailAddress); 100 | Assert.AreEqual("James 1", email.Data.ToAddresses[0].Name); 101 | Assert.AreEqual(string.Empty, email.Data.ToAddresses[1].Name); 102 | Assert.AreEqual("Fred", email.Data.ToAddresses[2].Name); 103 | } 104 | #endregion 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/AttachmentTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using System.Reflection; 4 | using FluentEmail.Core.Models; 5 | using NUnit.Framework; 6 | 7 | namespace FluentEmail.Core.Tests 8 | { 9 | [TestFixture] 10 | public class AttachmentTests 11 | { 12 | private Assembly ThisAssembly() => this.GetType().GetTypeInfo().Assembly; 13 | const string toEmail = "bob@test.com"; 14 | const string fromEmail = "johno@test.com"; 15 | const string subject = "sup dawg"; 16 | 17 | [Test] 18 | public void Attachment_from_stream_Is_set() 19 | { 20 | using (var stream = File.OpenRead($"{Path.Combine(Directory.GetCurrentDirectory(), "test.txt")}")) 21 | { 22 | var attachment = new Attachment 23 | { 24 | Data = stream, 25 | Filename = "Test.txt", 26 | ContentType = "text/plain" 27 | }; 28 | 29 | var email = Email.From(fromEmail) 30 | .To(toEmail) 31 | .Subject(subject) 32 | .Attach(attachment); 33 | 34 | Assert.AreEqual(20, email.Data.Attachments.First().Data.Length); 35 | } 36 | } 37 | 38 | [Test] 39 | public void Attachment_from_filename_Is_set() 40 | { 41 | var email = Email.From(fromEmail) 42 | .To(toEmail) 43 | .Subject(subject) 44 | .AttachFromFilename($"{Path.Combine(Directory.GetCurrentDirectory(), "test.txt")}", "text/plain"); 45 | 46 | Assert.AreEqual(20, email.Data.Attachments.First().Data.Length); 47 | } 48 | 49 | [Test] 50 | public void Attachment_from_filename_AttachmentName_Is_set() 51 | { 52 | var attachmentName = "attachment.txt"; 53 | var email = Email.From(fromEmail) 54 | .To(toEmail) 55 | .Subject(subject) 56 | .AttachFromFilename($"{Path.Combine(Directory.GetCurrentDirectory(), "test.txt")}", "text/plain", attachmentName); 57 | 58 | Assert.AreEqual(20, email.Data.Attachments.First().Data.Length); 59 | Assert.AreEqual(attachmentName, email.Data.Attachments.First().Filename); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/FluentEmail.Core.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | true 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 | PreserveNewest 31 | 32 | 33 | 34 | PreserveNewest 35 | 36 | 37 | PreserveNewest 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/FluentEmailTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using FluentEmail.Core.Models; 3 | using NUnit.Framework; 4 | using System.Linq; 5 | 6 | namespace FluentEmail.Core.Tests 7 | { 8 | [TestFixture] 9 | public class FluentEmailTests 10 | { 11 | const string toEmail = "bob@test.com"; 12 | const string fromEmail = "johno@test.com"; 13 | const string subject = "sup dawg"; 14 | const string body = "what be the hipitity hap?"; 15 | 16 | [Test] 17 | public void To_Address_Is_Set() 18 | { 19 | var email = Email 20 | .From(fromEmail) 21 | .To(toEmail); 22 | 23 | Assert.AreEqual(toEmail, email.Data.ToAddresses[0].EmailAddress); 24 | } 25 | 26 | [Test] 27 | public void From_Address_Is_Set() 28 | { 29 | var email = Email.From(fromEmail); 30 | 31 | Assert.AreEqual(fromEmail, email.Data.FromAddress.EmailAddress); 32 | } 33 | 34 | [Test] 35 | public void Subject_Is_Set() 36 | { 37 | var email = Email 38 | .From(fromEmail) 39 | .Subject(subject); 40 | 41 | Assert.AreEqual(subject, email.Data.Subject); 42 | } 43 | 44 | [Test] 45 | public void Body_Is_Set() 46 | { 47 | var email = Email.From(fromEmail) 48 | .Body(body); 49 | 50 | Assert.AreEqual(body, email.Data.Body); 51 | } 52 | 53 | [Test] 54 | public void Can_Add_Multiple_Recipients() 55 | { 56 | string toEmail1 = "bob@test.com"; 57 | string toEmail2 = "ratface@test.com"; 58 | 59 | var email = Email 60 | .From(fromEmail) 61 | .To(toEmail1) 62 | .To(toEmail2); 63 | 64 | Assert.AreEqual(2, email.Data.ToAddresses.Count); 65 | } 66 | 67 | [Test] 68 | public void Can_Add_Multiple_Recipients_From_List() 69 | { 70 | var emails = new List
(); 71 | emails.Add(new Address("email1@email.com")); 72 | emails.Add(new Address("email2@email.com")); 73 | 74 | var email = Email 75 | .From(fromEmail) 76 | .To(emails); 77 | 78 | Assert.AreEqual(2, email.Data.ToAddresses.Count); 79 | } 80 | 81 | [Test] 82 | public void Can_Add_Multiple_CCRecipients_From_List() 83 | { 84 | var emails = new List
(); 85 | emails.Add(new Address("email1@email.com")); 86 | emails.Add(new Address("email2@email.com")); 87 | 88 | var email = Email 89 | .From(fromEmail) 90 | .CC(emails); 91 | 92 | Assert.AreEqual(2, email.Data.CcAddresses.Count); 93 | } 94 | 95 | [Test] 96 | public void Can_Add_Multiple_BCCRecipients_From_List() 97 | { 98 | var emails = new List
(); 99 | emails.Add(new Address("email1@email.com")); 100 | emails.Add(new Address("email2@email.com")); 101 | 102 | var email = Email 103 | .From(fromEmail) 104 | .BCC(emails); 105 | 106 | Assert.AreEqual(2, email.Data.BccAddresses.Count); 107 | } 108 | 109 | [Test] 110 | public void Is_Valid_With_Properties_Set() 111 | { 112 | var email = Email 113 | .From(fromEmail) 114 | .To(toEmail) 115 | .Subject(subject) 116 | .Body(body); 117 | 118 | Assert.AreEqual(body, email.Data.Body); 119 | Assert.AreEqual(subject, email.Data.Subject); 120 | Assert.AreEqual(fromEmail, email.Data.FromAddress.EmailAddress); 121 | Assert.AreEqual(toEmail, email.Data.ToAddresses[0].EmailAddress); 122 | } 123 | 124 | [Test] 125 | public void ReplyTo_Address_Is_Set() 126 | { 127 | var replyEmail = "reply@email.com"; 128 | 129 | var email = Email.From(fromEmail) 130 | .ReplyTo(replyEmail); 131 | 132 | Assert.AreEqual(replyEmail, email.Data.ReplyToAddresses.First().EmailAddress); 133 | } 134 | 135 | #region Refactored tests using setup through constructors. 136 | [Test] 137 | public void New_To_Address_Is_Set() 138 | { 139 | var email = new Email(fromEmail) 140 | .To(toEmail); 141 | 142 | Assert.AreEqual(toEmail, email.Data.ToAddresses[0].EmailAddress); 143 | } 144 | 145 | [Test] 146 | public void New_From_Address_Is_Set() 147 | { 148 | var email = new Email(fromEmail); 149 | 150 | Assert.AreEqual(fromEmail, email.Data.FromAddress.EmailAddress); 151 | } 152 | 153 | [Test] 154 | public void New_Subject_Is_Set() 155 | { 156 | var email = new Email(fromEmail) 157 | .Subject(subject); 158 | 159 | Assert.AreEqual(subject, email.Data.Subject); 160 | } 161 | 162 | [Test] 163 | public void New_Body_Is_Set() 164 | { 165 | var email = new Email(fromEmail) 166 | .Body(body); 167 | 168 | Assert.AreEqual(body, email.Data.Body); 169 | } 170 | 171 | [Test] 172 | public void New_Can_Add_Multiple_Recipients() 173 | { 174 | string toEmail1 = "bob@test.com"; 175 | string toEmail2 = "ratface@test.com"; 176 | 177 | var email = new Email(fromEmail) 178 | .To(toEmail1) 179 | .To(toEmail2); 180 | 181 | Assert.AreEqual(2, email.Data.ToAddresses.Count); 182 | } 183 | 184 | [Test] 185 | public void New_Can_Add_Multiple_Recipients_From_List() 186 | { 187 | var emails = new List
(); 188 | emails.Add(new Address("email1@email.com")); 189 | emails.Add(new Address("email2@email.com")); 190 | 191 | var email = new Email(fromEmail) 192 | .To(emails); 193 | 194 | Assert.AreEqual(2, email.Data.ToAddresses.Count); 195 | } 196 | 197 | [Test] 198 | public void New_Can_Add_Multiple_CCRecipients_From_List() 199 | { 200 | var emails = new List
(); 201 | emails.Add(new Address("email1@email.com")); 202 | emails.Add(new Address("email2@email.com")); 203 | 204 | var email = new Email(fromEmail) 205 | .CC(emails); 206 | 207 | Assert.AreEqual(2, email.Data.CcAddresses.Count); 208 | } 209 | 210 | [Test] 211 | public void New_Can_Add_Multiple_BCCRecipients_From_List() 212 | { 213 | var emails = new List
(); 214 | emails.Add(new Address("email1@email.com")); 215 | emails.Add(new Address("email2@email.com")); 216 | 217 | var email = new Email(fromEmail) 218 | .BCC(emails); 219 | 220 | Assert.AreEqual(2, email.Data.BccAddresses.Count); 221 | } 222 | 223 | [Test] 224 | public void New_Is_Valid_With_Properties_Set() 225 | { 226 | var email = new Email(fromEmail) 227 | .To(toEmail) 228 | .Subject(subject) 229 | .Body(body); 230 | 231 | Assert.AreEqual(body, email.Data.Body); 232 | Assert.AreEqual(subject, email.Data.Subject); 233 | Assert.AreEqual(fromEmail, email.Data.FromAddress.EmailAddress); 234 | Assert.AreEqual(toEmail, email.Data.ToAddresses[0].EmailAddress); 235 | } 236 | 237 | [Test] 238 | public void New_ReplyTo_Address_Is_Set() 239 | { 240 | var replyEmail = "reply@email.com"; 241 | 242 | var email = new Email(fromEmail) 243 | .ReplyTo(replyEmail); 244 | 245 | Assert.AreEqual(replyEmail, email.Data.ReplyToAddresses.First().EmailAddress); 246 | } 247 | #endregion 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/GraphSenderTests.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core; 2 | using FluentEmail.Core.Models; 3 | using NUnit.Framework; 4 | using System; 5 | using System.IO; 6 | using System.Threading.Tasks; 7 | 8 | namespace FluentEmail.Graph.Tests 9 | { 10 | public class Tests 11 | { 12 | //TODO: For these tests to pass you will need to supply the following details from an Azure AD / Office 365 Tenant 13 | const string appId = ""; //Add your AAD Graph App ID here 14 | const string tenantId = ""; //Add your AAD Tenant ID here 15 | const string graphSecret = ""; //Add your AAD Graph Client Secret here 16 | const string senderEmail = ""; //Add a sender email address from your Office 365 tenant 17 | const string toEmail = "fluentemail@mailinator.com"; //change this if you like 18 | private bool saveSent = false; 19 | 20 | [SetUp] 21 | public void Setup() 22 | { 23 | if (string.IsNullOrWhiteSpace(appId)) throw new ArgumentException("Graph App ID needs to be supplied"); 24 | if (string.IsNullOrWhiteSpace(tenantId)) throw new ArgumentException("Graph tenant ID needs to be supplied"); 25 | if (string.IsNullOrWhiteSpace(graphSecret)) throw new ArgumentException("Graph client secret needs to be supplied"); 26 | if (string.IsNullOrWhiteSpace(senderEmail)) throw new ArgumentException("Sender email address needs to be supplied"); 27 | 28 | var sender = new GraphSender(appId, tenantId, graphSecret, saveSent); 29 | 30 | Email.DefaultSender = sender; 31 | } 32 | 33 | [Test, Ignore("Missing Graph credentials")] 34 | public void CanSendEmail() 35 | { 36 | var email = Email 37 | .From(senderEmail) 38 | .To(toEmail) 39 | .Subject("Test Email") 40 | .Body("Test email from Graph sender unit test"); 41 | 42 | var response = email.Send(); 43 | Assert.IsTrue(response.Successful); 44 | } 45 | 46 | [Test, Ignore("Missing Graph credentials")] 47 | public async Task CanSendEmailAsync() 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 | var response = await email.SendAsync(); 56 | Assert.IsTrue(response.Successful); 57 | } 58 | 59 | [Test, Ignore("Missing Graph credentials")] 60 | public async Task CanSendEmailWithAttachments() 61 | { 62 | var stream = new MemoryStream(); 63 | var sw = new StreamWriter(stream); 64 | sw.WriteLine("Hey this is some text in an attachment"); 65 | sw.Flush(); 66 | stream.Seek(0, SeekOrigin.Begin); 67 | 68 | var attachment = new Attachment 69 | { 70 | ContentType = "text/plain", 71 | Filename = "graphtest.txt", 72 | Data = stream 73 | }; 74 | 75 | var email = Email 76 | .From(senderEmail) 77 | .To(toEmail) 78 | .Subject("Test Email with Attachments") 79 | .Body("Test email from Graph sender unit test") 80 | .Attach(attachment); 81 | 82 | var response = await email.SendAsync(); 83 | Assert.IsTrue(response.Successful); 84 | } 85 | 86 | [Test, Ignore("Missing Graph credentials")] 87 | public async Task CanSendHighPriorityEmail() 88 | { 89 | var email = Email 90 | .From(senderEmail) 91 | .To(toEmail) 92 | .Subject("Test High Priority Email") 93 | .Body("Test email from Graph sender unit test") 94 | .HighPriority(); 95 | 96 | var response = await email.SendAsync(); 97 | Assert.IsTrue(response.Successful); 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/MailKitSmtpSenderTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using FluentEmail.Core; 4 | using FluentEmail.MailKitSmtp; 5 | using NUnit.Framework; 6 | using Attachment = FluentEmail.Core.Models.Attachment; 7 | 8 | namespace FluentEmail.MailKit.Tests 9 | { 10 | [NonParallelizable] 11 | public class MailKitSmtpSenderTests 12 | { 13 | // Warning: To pass, an smtp listener must be running on localhost:25. 14 | 15 | const string toEmail = "bob@test.com"; 16 | const string fromEmail = "johno@test.com"; 17 | const string subject = "sup dawg"; 18 | const string body = "what be the hipitity hap?"; 19 | 20 | private readonly string tempDirectory; 21 | 22 | public MailKitSmtpSenderTests() 23 | { 24 | tempDirectory = Path.Combine(Path.GetTempPath(), "EmailTest"); 25 | } 26 | 27 | [SetUp] 28 | public void SetUp() 29 | { 30 | var sender = new MailKitSender(new SmtpClientOptions 31 | { 32 | Server = "localhost", 33 | Port = 25, 34 | UseSsl = false, 35 | RequiresAuthentication = false, 36 | UsePickupDirectory = true, 37 | MailPickupDirectory = Path.Combine(Path.GetTempPath(), "EmailTest") 38 | }); 39 | 40 | Email.DefaultSender = sender; 41 | Directory.CreateDirectory(tempDirectory); 42 | } 43 | 44 | [TearDown] 45 | public void TearDown() 46 | { 47 | Directory.Delete(tempDirectory, true); 48 | } 49 | 50 | [Test] 51 | public void CanSendEmail() 52 | { 53 | var email = Email 54 | .From(fromEmail) 55 | .To(toEmail) 56 | .Body("

Test

", true); 57 | 58 | var response = email.Send(); 59 | 60 | var files = Directory.EnumerateFiles(tempDirectory, "*.eml"); 61 | Assert.IsTrue(response.Successful); 62 | Assert.IsNotEmpty(files); 63 | } 64 | 65 | [Test] 66 | public async Task CanSendEmailWithAttachments() 67 | { 68 | var stream = new MemoryStream(); 69 | var sw = new StreamWriter(stream); 70 | sw.WriteLine("Hey this is some text in an attachment"); 71 | sw.Flush(); 72 | stream.Seek(0, SeekOrigin.Begin); 73 | 74 | var attachment = new Attachment 75 | { 76 | Data = stream, 77 | ContentType = "text/plain", 78 | Filename = "MailKitAttachment.txt" 79 | }; 80 | 81 | var email = Email 82 | .From(fromEmail) 83 | .To(toEmail) 84 | .Subject(subject) 85 | .Body(body) 86 | .Attach(attachment); 87 | 88 | var response = await email.SendAsync(); 89 | 90 | var files = Directory.EnumerateFiles(tempDirectory, "*.eml"); 91 | Assert.IsTrue(response.Successful); 92 | Assert.IsNotEmpty(files); 93 | } 94 | 95 | [Test] 96 | public async Task CanSendAsyncHtmlAndPlaintextTogether() 97 | { 98 | var email = Email 99 | .From(fromEmail) 100 | .To(toEmail) 101 | .Body("

Test

some body text

", true) 102 | .PlaintextAlternativeBody("Test - Some body text"); 103 | 104 | var response = await email.SendAsync(); 105 | 106 | Assert.IsTrue(response.Successful); 107 | } 108 | 109 | [Test] 110 | public void CanSendHtmlAndPlaintextTogether() 111 | { 112 | var email = Email 113 | .From(fromEmail) 114 | .To(toEmail) 115 | .Body("

Test

some body text

", true) 116 | .PlaintextAlternativeBody("Test - Some body text"); 117 | 118 | var response = email.Send(); 119 | 120 | Assert.IsTrue(response.Successful); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/MailgunSenderTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using FluentEmail.Core; 4 | using FluentEmail.Core.Models; 5 | using NUnit.Framework; 6 | using Newtonsoft.Json; 7 | 8 | namespace FluentEmail.Mailgun.Tests 9 | { 10 | public class MailgunSenderTests 11 | { 12 | const string toEmail = "bentest1@mailinator.com"; 13 | const string fromEmail = "ben@test.com"; 14 | const string subject = "Attachment Tests"; 15 | const string body = "This email is testing the attachment functionality of MailGun."; 16 | 17 | [SetUp] 18 | public void SetUp() 19 | { 20 | var sender = new MailgunSender("sandboxcf5f41bbf2f84f15a386c60e253b5fe9.mailgun.org", "key-8d32c046d7f14ada8d5ba8253e3e30de"); 21 | Email.DefaultSender = sender; 22 | } 23 | 24 | [Test] 25 | public async Task CanSendEmail() 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 | Assert.IsTrue(response.Successful); 36 | } 37 | 38 | [Test] 39 | public async Task GetMessageIdInResponse() 40 | { 41 | var email = Email 42 | .From(fromEmail) 43 | .To(toEmail) 44 | .Subject(subject) 45 | .Body(body); 46 | 47 | var response = await email.SendAsync(); 48 | 49 | Assert.IsTrue(response.Successful); 50 | Assert.IsNotEmpty(response.MessageId); 51 | } 52 | 53 | [Test] 54 | public async Task CanSendEmailWithTag() 55 | { 56 | var email = Email 57 | .From(fromEmail) 58 | .To(toEmail) 59 | .Subject(subject) 60 | .Body(body) 61 | .Tag("test"); 62 | 63 | var response = await email.SendAsync(); 64 | 65 | Assert.IsTrue(response.Successful); 66 | } 67 | 68 | [Test] 69 | public async Task CanSendEmailWithVariables() 70 | { 71 | var email = Email 72 | .From(fromEmail) 73 | .To(toEmail) 74 | .Subject(subject) 75 | .Body(body) 76 | .Header("X-Mailgun-Variables", JsonConvert.SerializeObject(new Variable { Var1 = "Test"})); 77 | 78 | var response = await email.SendAsync(); 79 | 80 | Assert.IsTrue(response.Successful); 81 | } 82 | 83 | [Test] 84 | public async Task CanSendEmailWithAttachments() 85 | { 86 | var stream = new MemoryStream(); 87 | var sw = new StreamWriter(stream); 88 | sw.WriteLine("Hey this is some text in an attachment"); 89 | sw.Flush(); 90 | stream.Seek(0, SeekOrigin.Begin); 91 | 92 | var attachment = new Attachment 93 | { 94 | Data = stream, 95 | ContentType = "text/plain", 96 | Filename = "mailgunTest.txt" 97 | }; 98 | 99 | var email = Email 100 | .From(fromEmail) 101 | .To(toEmail) 102 | .Subject(subject) 103 | .Body(body) 104 | .Attach(attachment); 105 | 106 | var response = await email.SendAsync(); 107 | 108 | Assert.IsTrue(response.Successful); 109 | } 110 | 111 | [Test] 112 | public async Task CanSendEmailWithInlineImages() 113 | { 114 | using (var stream = File.OpenRead($"{Path.Combine(Directory.GetCurrentDirectory(), "logotest.png")}")) 115 | { 116 | var attachment = new Attachment 117 | { 118 | IsInline = true, 119 | Data = stream, 120 | ContentType = "image/png", 121 | Filename = "logotest.png" 122 | }; 123 | 124 | var email = Email 125 | .From(fromEmail) 126 | .To(toEmail) 127 | .Subject(subject) 128 | .Body("Inline image here: " + 129 | "

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

", true) 130 | .Attach(attachment); 131 | 132 | var response = await email.SendAsync(); 133 | 134 | Assert.IsTrue(response.Successful); 135 | } 136 | } 137 | 138 | class Variable 139 | { 140 | public string Var1 { get; set; } 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/MailtrapSenderTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using FluentEmail.Core; 4 | using FluentEmail.Core.Models; 5 | using NUnit.Framework; 6 | 7 | namespace FluentEmail.Mailtrap.Tests 8 | { 9 | public class MailtrapSenderTests 10 | { 11 | const string toEmail = "testto.fluentemail@mailinator.com"; 12 | const string fromEmail = "testfrom.fluentemail@mailinator.com"; 13 | const string subject = "Mailtrap Email Test"; 14 | const string body = "This email is testing the functionality of mailtrap."; 15 | const string username = ""; // Mailtrap SMTP inbox username 16 | const string password = ""; // Mailtrap SMTP inbox password 17 | 18 | [SetUp] 19 | public void SetUp() 20 | { 21 | var sender = new MailtrapSender(username, password); 22 | Email.DefaultSender = sender; 23 | } 24 | 25 | [Test, Ignore("Missing credentials")] 26 | public void CanSendEmail() 27 | { 28 | var email = Email 29 | .From(fromEmail) 30 | .To(toEmail) 31 | .Subject(subject) 32 | .Body(body); 33 | 34 | var response = email.Send(); 35 | 36 | Assert.IsTrue(response.Successful); 37 | } 38 | 39 | 40 | [Test, Ignore("Missing credentials")] 41 | public async Task CanSendEmailAsync() 42 | { 43 | var email = Email 44 | .From(fromEmail) 45 | .To(toEmail) 46 | .Subject(subject) 47 | .Body(body); 48 | 49 | var response = await email.SendAsync(); 50 | 51 | Assert.IsTrue(response.Successful); 52 | } 53 | 54 | [Test, Ignore("Missing credentials")] 55 | public async Task CanSendEmailWithAttachments() 56 | { 57 | var stream = new MemoryStream(); 58 | var sw = new StreamWriter(stream); 59 | sw.WriteLine("Hey this is some text in an attachment"); 60 | sw.Flush(); 61 | stream.Seek(0, SeekOrigin.Begin); 62 | 63 | var attachment = new Attachment 64 | { 65 | Data = stream, 66 | ContentType = "text/plain", 67 | Filename = "mailtrapTest.txt" 68 | }; 69 | 70 | var email = Email 71 | .From(fromEmail) 72 | .To(toEmail) 73 | .Subject(subject) 74 | .Body(body) 75 | .Attach(attachment); 76 | 77 | var response = await email.SendAsync(); 78 | 79 | Assert.IsTrue(response.Successful); 80 | } 81 | 82 | [Test, Ignore("Missing credentials")] 83 | public async Task CanSendEmailWithInlineImages() 84 | { 85 | using (var stream = File.OpenRead($"{Path.Combine(Directory.GetCurrentDirectory(), "logotest.png")}")) 86 | { 87 | var attachment = new Attachment 88 | { 89 | IsInline = true, 90 | Data = stream, 91 | ContentType = "image/png", 92 | Filename = "logotest.png" 93 | }; 94 | 95 | var email = Email 96 | .From(fromEmail) 97 | .To(toEmail) 98 | .Subject(subject) 99 | .Body("Inline image here: " + 100 | "

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

", true) 101 | .Attach(attachment); 102 | 103 | var response = await email.SendAsync(); 104 | 105 | Assert.IsTrue(response.Successful); 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/SendGridSenderTests.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core; 2 | using NUnit.Framework; 3 | using System; 4 | using System.IO; 5 | using System.Threading.Tasks; 6 | using Attachment = FluentEmail.Core.Models.Attachment; 7 | 8 | namespace FluentEmail.SendGrid.Tests 9 | { 10 | public class SendGridSenderTests 11 | { 12 | const string apiKey = "missing-credentials"; // TODO: Put your API key here 13 | 14 | const string toEmail = "fluentEmail@mailinator.com"; 15 | const string toName = "FluentEmail Mailinator"; 16 | const string fromEmail = "test@fluentmail.com"; 17 | const string fromName = "SendGridSender Test"; 18 | 19 | [SetUp] 20 | public void SetUp() 21 | { 22 | if (string.IsNullOrWhiteSpace(apiKey)) throw new ArgumentException("SendGrid Api Key needs to be supplied"); 23 | 24 | var sender = new SendGridSender(apiKey, true); 25 | Email.DefaultSender = sender; 26 | } 27 | 28 | [Test, Ignore("No sendgrid credentials")] 29 | public async Task CanSendEmail() 30 | { 31 | const string subject = "SendMail Test"; 32 | const string body = "This email is testing send mail functionality of SendGrid Sender."; 33 | 34 | var email = Email 35 | .From(fromEmail, fromName) 36 | .To(toEmail, toName) 37 | .Subject(subject) 38 | .Body(body); 39 | 40 | var response = await email.SendAsync(); 41 | 42 | Assert.IsTrue(response.Successful); 43 | } 44 | 45 | [Test, Ignore("No sendgrid credentials")] 46 | public async Task CanSendTemplateEmail() 47 | { 48 | const string subject = "SendMail Test"; 49 | const string templateId = "123456-insert-your-own-id-here"; 50 | object templateData = new 51 | { 52 | Name = toName, 53 | ArbitraryValue = "The quick brown fox jumps over the lazy dog." 54 | }; 55 | 56 | var email = Email 57 | .From(fromEmail, fromName) 58 | .To(toEmail, toName) 59 | .Subject(subject); 60 | 61 | var response = await email.SendWithTemplateAsync(templateId, templateData); 62 | 63 | Assert.IsTrue(response.Successful); 64 | } 65 | 66 | [Test, Ignore("No sendgrid credentials")] 67 | public async Task CanSendEmailWithReplyTo() 68 | { 69 | const string subject = "SendMail Test"; 70 | const string body = "This email is testing send mail with ReplyTo functionality of SendGrid Sender."; 71 | 72 | var email = Email 73 | .From(fromEmail, fromName) 74 | .To(toEmail, toName) 75 | .ReplyTo(toEmail, toName) 76 | .Subject(subject) 77 | .Body(body); 78 | 79 | var response = await email.SendAsync(); 80 | 81 | Assert.IsTrue(response.Successful); 82 | } 83 | 84 | [Test, Ignore("No sendgrid credentials")] 85 | public async Task CanSendEmailWithCategory() 86 | { 87 | const string subject = "SendMail Test"; 88 | const string body = "This email is testing send mail with Categories functionality of SendGrid Sender."; 89 | 90 | var email = Email 91 | .From(fromEmail, fromName) 92 | .To(toEmail, toName) 93 | .ReplyTo(toEmail, toName) 94 | .Subject(subject) 95 | .Tag("TestCategory") 96 | .Body(body); 97 | 98 | var response = await email.SendAsync(); 99 | 100 | Assert.IsTrue(response.Successful); 101 | } 102 | 103 | [Test, Ignore("No sendgrid credentials")] 104 | public async Task CanSendEmailWithAttachments() 105 | { 106 | const string subject = "SendMail With Attachments Test"; 107 | const string body = "This email is testing the attachment functionality of SendGrid Sender."; 108 | 109 | using (var stream = File.OpenRead($"{Directory.GetCurrentDirectory()}/test-binary.xlsx")) 110 | { 111 | var attachment = new Attachment 112 | { 113 | Data = stream, 114 | ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 115 | Filename = "test-binary.xlsx" 116 | }; 117 | 118 | var email = Email 119 | .From(fromEmail, fromName) 120 | .To(toEmail, toName) 121 | .Subject(subject) 122 | .Body(body) 123 | .Attach(attachment); 124 | 125 | 126 | var response = await email.SendAsync(); 127 | 128 | Assert.IsTrue(response.Successful); 129 | } 130 | } 131 | 132 | [Test, Ignore("No sendgrid credentials")] 133 | public async Task CanSendHighPriorityEmail() 134 | { 135 | const string subject = "SendMail Test"; 136 | const string body = "This email is testing send mail functionality of SendGrid Sender."; 137 | 138 | var email = Email 139 | .From(fromEmail, fromName) 140 | .To(toEmail, toName) 141 | .Subject(subject) 142 | .Body(body) 143 | .HighPriority(); 144 | 145 | var response = await email.SendAsync(); 146 | 147 | Assert.IsTrue(response.Successful); 148 | } 149 | 150 | [Test, Ignore("No sendgrid credentials")] 151 | public async Task CanSendLowPriorityEmail() 152 | { 153 | const string subject = "SendMail Test"; 154 | const string body = "This email is testing send mail functionality of SendGrid Sender."; 155 | 156 | var email = Email 157 | .From(fromEmail, fromName) 158 | .To(toEmail, toName) 159 | .Subject(subject) 160 | .Body(body) 161 | .LowPriority(); 162 | 163 | var response = await email.SendAsync(); 164 | 165 | Assert.IsTrue(response.Successful); 166 | } 167 | } 168 | } -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/SmtpSenderTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Net.Mail; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using FluentEmail.Core; 6 | using NUnit.Framework; 7 | using Attachment = FluentEmail.Core.Models.Attachment; 8 | 9 | namespace FluentEmail.Smtp.Tests 10 | { 11 | [NonParallelizable] 12 | public class SmtpSenderTests 13 | { 14 | // Warning: To pass, an smtp listener must be running on localhost:25. 15 | 16 | const string toEmail = "bob@test.com"; 17 | const string fromEmail = "johno@test.com"; 18 | const string subject = "sup dawg"; 19 | const string body = "what be the hipitity hap?"; 20 | 21 | private static IFluentEmail TestEmail => Email 22 | .From(fromEmail) 23 | .To(toEmail) 24 | .Subject(subject) 25 | .Body(body); 26 | 27 | private readonly string tempDirectory; 28 | 29 | public SmtpSenderTests() 30 | { 31 | tempDirectory = Path.Combine(Path.GetTempPath(), "EmailTest"); 32 | } 33 | 34 | [SetUp] 35 | public void SetUp() 36 | { 37 | var sender = new SmtpSender(() => new SmtpClient("localhost") 38 | { 39 | EnableSsl = false, 40 | DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory, 41 | PickupDirectoryLocation = tempDirectory 42 | }); 43 | 44 | Email.DefaultSender = sender; 45 | Directory.CreateDirectory(tempDirectory); 46 | } 47 | 48 | [TearDown] 49 | public void TearDown() 50 | { 51 | Directory.Delete(tempDirectory, true); 52 | } 53 | 54 | [Test] 55 | public void CanSendEmail() 56 | { 57 | var email = TestEmail 58 | .Body("

Test

", true); 59 | 60 | var response = email.Send(); 61 | 62 | var files = Directory.EnumerateFiles(tempDirectory, "*.eml"); 63 | Assert.IsTrue(response.Successful); 64 | Assert.IsNotEmpty(files); 65 | } 66 | 67 | [Test] 68 | public async Task CanSendEmailWithAttachments() 69 | { 70 | var stream = new MemoryStream(); 71 | var sw = new StreamWriter(stream); 72 | sw.WriteLine("Hey this is some text in an attachment"); 73 | sw.Flush(); 74 | stream.Seek(0, SeekOrigin.Begin); 75 | 76 | var attachment = new Attachment 77 | { 78 | Data = stream, 79 | ContentType = "text/plain", 80 | Filename = "mailgunTest.txt" 81 | }; 82 | 83 | var email = TestEmail 84 | .Attach(attachment); 85 | 86 | var response = await email.SendAsync(); 87 | 88 | Assert.IsTrue(response.Successful); 89 | var files = Directory.EnumerateFiles(tempDirectory, "*.eml"); 90 | Assert.IsNotEmpty(files); 91 | } 92 | 93 | [Test] 94 | public async Task CanSendAsyncHtmlAndPlaintextTogether() 95 | { 96 | var email = TestEmail 97 | .Body("

Test

some body text

", true) 98 | .PlaintextAlternativeBody("Test - Some body text"); 99 | 100 | var response = await email.SendAsync(); 101 | 102 | Assert.IsTrue(response.Successful); 103 | } 104 | 105 | [Test] 106 | public void CanSendHtmlAndPlaintextTogether() 107 | { 108 | var email = TestEmail 109 | .Body("

Test

some body text

", true) 110 | .PlaintextAlternativeBody("Test - Some body text"); 111 | 112 | var response = email.Send(); 113 | 114 | Assert.IsTrue(response.Successful); 115 | } 116 | 117 | [Test] 118 | public void CancelSendIfCancelationRequested() 119 | { 120 | var email = TestEmail; 121 | 122 | var tokenSource = new CancellationTokenSource(); 123 | tokenSource.Cancel(); 124 | 125 | var response = email.Send(tokenSource.Token); 126 | 127 | Assert.IsFalse(response.Successful); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/TemplateEmailTests.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Threading.Tasks; 5 | using FluentEmail.Core.Defaults; 6 | using FluentEmail.Core.Interfaces; 7 | using NUnit.Framework; 8 | 9 | namespace FluentEmail.Core.Tests 10 | { 11 | [TestFixture] 12 | public class TemplateEmailTests 13 | { 14 | private Assembly ThisAssembly() => this.GetType().GetTypeInfo().Assembly; 15 | const string toEmail = "bob@test.com"; 16 | const string fromEmail = "johno@test.com"; 17 | const string subject = "sup dawg"; 18 | 19 | [Test] 20 | public void Anonymous_Model_Template_From_File_Matches() 21 | { 22 | var email = Email 23 | .From(fromEmail) 24 | .To(toEmail) 25 | .Subject(subject) 26 | .UsingTemplateFromFile($"{Path.Combine(Directory.GetCurrentDirectory(), "test.txt")}", new { Test = "FLUENTEMAIL" }); 27 | 28 | Assert.AreEqual("yo email FLUENTEMAIL", email.Data.Body); 29 | } 30 | 31 | [Test] 32 | public void Using_Template_From_Not_Existing_Culture_File_Using_Default_Template() 33 | { 34 | var culture = new CultureInfo("fr-FR"); 35 | var email = Email 36 | .From(fromEmail) 37 | .To(toEmail) 38 | .Subject(subject) 39 | .UsingCultureTemplateFromFile($"{Path.Combine(Directory.GetCurrentDirectory(), "test.txt")}", new { Test = "FLUENTEMAIL", culture }, culture); 40 | 41 | Assert.AreEqual("yo email FLUENTEMAIL", email.Data.Body); 42 | } 43 | 44 | [Test] 45 | public void Using_Template_From_Culture_File() 46 | { 47 | var culture = new CultureInfo("he-IL"); 48 | var email = Email 49 | .From(fromEmail) 50 | .To(toEmail) 51 | .Subject(subject) 52 | .UsingCultureTemplateFromFile($"{Path.Combine(Directory.GetCurrentDirectory(), "test.txt")}", new { Test = "FLUENTEMAIL" }, culture); 53 | 54 | Assert.AreEqual("hebrew email FLUENTEMAIL", email.Data.Body); 55 | } 56 | 57 | [Test] 58 | public void Using_Template_From_Current_Culture_File() 59 | { 60 | var culture = new CultureInfo("he-IL"); 61 | var email = Email 62 | .From(fromEmail) 63 | .To(toEmail) 64 | .Subject(subject) 65 | .UsingCultureTemplateFromFile($"{Path.Combine(Directory.GetCurrentDirectory(), "test.txt")}", new {Test = "FLUENTEMAIL"}, culture); 66 | 67 | Assert.AreEqual("hebrew email FLUENTEMAIL", email.Data.Body); 68 | } 69 | 70 | [Test] 71 | public void Anonymous_Model_Template_Matches() 72 | { 73 | string template = "sup ##Name##"; 74 | 75 | var email = Email 76 | .From(fromEmail) 77 | .To(toEmail) 78 | .Subject(subject) 79 | .UsingTemplate(template, new { Name = "LUKE" }); 80 | 81 | Assert.AreEqual("sup LUKE", email.Data.Body); 82 | } 83 | 84 | 85 | 86 | [Test] 87 | public void Set_Custom_Template() 88 | { 89 | string template = "sup ##Name## here is a list @foreach(var i in Model.Numbers) { @i }"; 90 | 91 | var email = Email 92 | .From(fromEmail) 93 | .To(toEmail) 94 | .Subject(subject) 95 | .UsingTemplateEngine(new TestTemplate()) 96 | .UsingTemplate(template, new { Name = "LUKE", Numbers = new string[] { "1", "2", "3" } }); 97 | 98 | Assert.AreEqual("custom template", email.Data.Body); 99 | } 100 | 101 | [Test] 102 | public void Using_Template_From_Embedded_Resource() 103 | { 104 | var email = Email 105 | .From(fromEmail) 106 | .To(toEmail) 107 | .Subject(subject) 108 | .UsingTemplateFromEmbedded("FluentEmail.Core.Tests.test-embedded.txt", new { Test = "EMBEDDEDTEST" }, ThisAssembly()); 109 | 110 | Assert.AreEqual("yo email EMBEDDEDTEST", email.Data.Body); 111 | } 112 | 113 | [Test] 114 | public void New_Anonymous_Model_Template_From_File_Matches() 115 | { 116 | var email = new Email(fromEmail) 117 | .To(toEmail) 118 | .Subject(subject) 119 | .UsingTemplateFromFile($"{Path.Combine(Directory.GetCurrentDirectory(), "test.txt")}", new { Test = "FLUENTEMAIL" }); 120 | 121 | Assert.AreEqual("yo email FLUENTEMAIL", email.Data.Body); 122 | } 123 | 124 | [Test] 125 | public void New_Using_Template_From_Not_Existing_Culture_File_Using_Default_Template() 126 | { 127 | var culture = new CultureInfo("fr-FR"); 128 | var email = new Email(fromEmail) 129 | .To(toEmail) 130 | .Subject(subject) 131 | .UsingCultureTemplateFromFile($"{Path.Combine(Directory.GetCurrentDirectory(), "test.txt")}", new { Test = "FLUENTEMAIL", culture }, culture); 132 | 133 | Assert.AreEqual("yo email FLUENTEMAIL", email.Data.Body); 134 | } 135 | 136 | [Test] 137 | public void New_Using_Template_From_Culture_File() 138 | { 139 | var culture = new CultureInfo("he-IL"); 140 | var email = new Email(fromEmail) 141 | .To(toEmail) 142 | .Subject(subject) 143 | .UsingCultureTemplateFromFile($"{Path.Combine(Directory.GetCurrentDirectory(), "test.txt")}", new { Test = "FLUENTEMAIL" }, culture); 144 | 145 | Assert.AreEqual("hebrew email FLUENTEMAIL", email.Data.Body); 146 | } 147 | 148 | [Test] 149 | public void New_Using_Template_From_Current_Culture_File() 150 | { 151 | var culture = new CultureInfo("he-IL"); 152 | var email = new Email(fromEmail) 153 | .To(toEmail) 154 | .Subject(subject) 155 | .UsingCultureTemplateFromFile($"{Path.Combine(Directory.GetCurrentDirectory(), "test.txt")}", new {Test = "FLUENTEMAIL"}, culture); 156 | 157 | Assert.AreEqual("hebrew email FLUENTEMAIL", email.Data.Body); 158 | } 159 | 160 | 161 | 162 | [Test] 163 | public void New_Set_Custom_Template() 164 | { 165 | string template = "sup @Model.Name here is a list @foreach(var i in Model.Numbers) { @i }"; 166 | 167 | var email = new Email(new TestTemplate(), new SaveToDiskSender("/"), fromEmail) 168 | .To(toEmail) 169 | .Subject(subject) 170 | .UsingTemplate(template, new { Name = "LUKE", Numbers = new string[] { "1", "2", "3" } }); 171 | 172 | Assert.AreEqual("custom template", email.Data.Body); 173 | } 174 | 175 | [Test] 176 | public void New_Using_Template_From_Embedded_Resource() 177 | { 178 | var email = new Email(fromEmail) 179 | .To(toEmail) 180 | .Subject(subject) 181 | .UsingTemplateFromEmbedded("FluentEmail.Core.Tests.test-embedded.txt", new { Test = "EMBEDDEDTEST" }, ThisAssembly()); 182 | 183 | Assert.AreEqual("yo email EMBEDDEDTEST", email.Data.Body); 184 | } 185 | } 186 | 187 | public class TestTemplate : ITemplateRenderer 188 | { 189 | public string Parse(string template, T model, bool isHtml = true) 190 | { 191 | return "custom template"; 192 | } 193 | 194 | public Task ParseAsync(string template, T model, bool isHtml = true) 195 | { 196 | return Task.FromResult(Parse(template, model, isHtml)); 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/logotest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukencode/FluentEmail/e1379c4d81b7b02cf3e48cf9bd718c307215e4bb/test/FluentEmail.Core.Tests/logotest.png -------------------------------------------------------------------------------- /test/FluentEmail.Core.Tests/test-binary.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukencode/FluentEmail/e1379c4d81b7b02cf3e48cf9bd718c307215e4bb/test/FluentEmail.Core.Tests/test-binary.xlsx -------------------------------------------------------------------------------- /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.Core.Tests/test.txt: -------------------------------------------------------------------------------- 1 | yo email ##Test## -------------------------------------------------------------------------------- /test/FluentEmail.Liquid.Tests/ComplexModel/ComplexModelRenderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FluentAssertions; 4 | using FluentEmail.Core; 5 | using Fluid; 6 | using Microsoft.Extensions.FileProviders; 7 | using Microsoft.Extensions.Options; 8 | using NUnit.Framework; 9 | 10 | namespace FluentEmail.Liquid.Tests.ComplexModel 11 | { 12 | public class ComplexModelRenderTests 13 | { 14 | public ComplexModelRenderTests() 15 | { 16 | SetupRenderer(); 17 | } 18 | 19 | [Test] 20 | public void Can_Render_Complex_Model_Properties() 21 | { 22 | var model = new ParentModel 23 | { 24 | ParentName = new NameDetails { Firstname = "Luke", Surname = "Dinosaur" }, 25 | ChildrenNames = new List 26 | { 27 | new NameDetails { Firstname = "ChildFirstA", Surname = "ChildLastA" }, 28 | new NameDetails { Firstname = "ChildFirstB", Surname = "ChildLastB" } 29 | } 30 | }; 31 | 32 | var expected = @" 33 | Parent: Luke 34 | Children: 35 | 36 | * ChildFirstA ChildLastA 37 | * ChildFirstB ChildLastB 38 | "; 39 | 40 | var email = Email 41 | .From(TestData.FromEmail) 42 | .To(TestData.ToEmail) 43 | .Subject(TestData.Subject) 44 | .UsingTemplate(Template(), model); 45 | email.Data.Body.Should().Be(expected); 46 | } 47 | 48 | private string Template() 49 | { 50 | return @" 51 | Parent: {{ ParentName.Firstname }} 52 | Children: 53 | {% for Child in ChildrenNames %} 54 | * {{ Child.Firstname }} {{ Child.Surname }}{% endfor %} 55 | "; 56 | } 57 | 58 | private static void SetupRenderer( 59 | IFileProvider fileProvider = null, 60 | Action configureTemplateContext = null) 61 | { 62 | var options = new LiquidRendererOptions 63 | { 64 | FileProvider = fileProvider, 65 | ConfigureTemplateContext = configureTemplateContext, 66 | TemplateOptions = new TemplateOptions { MemberAccessStrategy = new UnsafeMemberAccessStrategy() } 67 | }; 68 | Email.DefaultRenderer = new LiquidRenderer(Options.Create(options)); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /test/FluentEmail.Liquid.Tests/ComplexModel/ParentModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace FluentEmail.Liquid.Tests 4 | { 5 | public class ParentModel 6 | { 7 | public string Id { get; set; } 8 | public NameDetails ParentName { get; set; } 9 | public List ChildrenNames { get; set; } = new List(); 10 | } 11 | 12 | public class NameDetails 13 | { 14 | public string Firstname { get; set; } 15 | public string Surname { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /test/FluentEmail.Liquid.Tests/ComplexModel/TestData.cs: -------------------------------------------------------------------------------- 1 | namespace FluentEmail.Liquid.Tests 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 | } 9 | } -------------------------------------------------------------------------------- /test/FluentEmail.Liquid.Tests/EmailTemplates/_embedded.liquid: -------------------------------------------------------------------------------- 1 | 

Hello!

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

Hello!

2 |
{% renderbody %}
-------------------------------------------------------------------------------- /test/FluentEmail.Liquid.Tests/FluentEmail.Liquid.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | true 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 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /test/FluentEmail.Liquid.Tests/LiquidTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using FluentEmail.Core; 5 | 6 | using Fluid; 7 | 8 | using Microsoft.Extensions.FileProviders; 9 | using Microsoft.Extensions.Options; 10 | 11 | using NUnit.Framework; 12 | 13 | namespace FluentEmail.Liquid.Tests 14 | { 15 | public class LiquidTests 16 | { 17 | private const string ToEmail = "bob@test.com"; 18 | private const string FromEmail = "johno@test.com"; 19 | private const string Subject = "sup dawg"; 20 | 21 | [SetUp] 22 | public void SetUp() 23 | { 24 | // default to have no file provider, only required when layout files are in use 25 | SetupRenderer(); 26 | } 27 | 28 | private static void SetupRenderer( 29 | IFileProvider fileProvider = null, 30 | Action configureTemplateContext = null) 31 | { 32 | var options = new LiquidRendererOptions 33 | { 34 | FileProvider = fileProvider, 35 | ConfigureTemplateContext = configureTemplateContext, 36 | }; 37 | Email.DefaultRenderer = new LiquidRenderer(Options.Create(options)); 38 | } 39 | 40 | [Test] 41 | public void Model_With_List_Template_Matches() 42 | { 43 | const string template = "sup {{ Name }} here is a list {% for i in Numbers %}{{ i }}{% endfor %}"; 44 | 45 | var email = Email 46 | .From(FromEmail) 47 | .To(ToEmail) 48 | .Subject(Subject) 49 | .UsingTemplate(template, new ViewModel { Name = "LUKE", Numbers = new[] { "1", "2", "3" } }); 50 | 51 | Assert.AreEqual("sup LUKE here is a list 123", email.Data.Body); 52 | } 53 | 54 | [Test] 55 | public void Custom_Context_Values() 56 | { 57 | SetupRenderer(new NullFileProvider(), (context, model) => 58 | { 59 | context.SetValue("FirstName", "Samantha"); 60 | context.SetValue("IntegerNumbers", new[] {3, 2, 1}); 61 | }); 62 | 63 | const string template = "sup {{ FirstName }} here is a list {% for i in IntegerNumbers %}{{ i }}{% endfor %}"; 64 | 65 | var email = Email 66 | .From(FromEmail) 67 | .To(ToEmail) 68 | .Subject(Subject) 69 | .UsingTemplate(template, new ViewModel { Name = "LUKE", Numbers = new[] { "1", "2", "3" } }); 70 | 71 | Assert.AreEqual("sup Samantha here is a list 321", email.Data.Body); 72 | } 73 | 74 | // currently not cached as Fluid is so fast, but can be added later 75 | [Test] 76 | public void Reuse_Cached_Templates() 77 | { 78 | const string template = "sup {{ Name }} here is a list {% for i in Numbers %}{{ i }}{% endfor %}"; 79 | const string template2 = "sup {{ Name }} this is the second template"; 80 | 81 | for (var i = 0; i < 10; i++) 82 | { 83 | var email = Email 84 | .From(FromEmail) 85 | .To(ToEmail) 86 | .Subject(Subject) 87 | .UsingTemplate(template, new ViewModel { Name = i.ToString(), Numbers = new[] { "1", "2", "3" } }); 88 | 89 | Assert.AreEqual("sup " + i + " here is a list 123", email.Data.Body); 90 | 91 | var email2 = Email 92 | .From(FromEmail) 93 | .To(ToEmail) 94 | .Subject(Subject) 95 | .UsingTemplate(template2, new ViewModel { Name = i.ToString() }); 96 | 97 | Assert.AreEqual("sup " + i + " this is the second template", email2.Data.Body); 98 | } 99 | } 100 | 101 | [Test] 102 | public void New_Model_Template_Matches() 103 | { 104 | const string template = "sup {{ Name }}"; 105 | 106 | var email = new Email(FromEmail) 107 | .To(ToEmail) 108 | .Subject(Subject) 109 | .UsingTemplate(template, new ViewModel { Name = "LUKE" }); 110 | 111 | Assert.AreEqual("sup LUKE", email.Data.Body); 112 | } 113 | 114 | [Test] 115 | public void New_Model_With_List_Template_Matches() 116 | { 117 | const string template = "sup {{ Name }} here is a list {% for i in Numbers %}{{ i }}{% endfor %}"; 118 | 119 | var email = new Email(FromEmail) 120 | .To(ToEmail) 121 | .Subject(Subject) 122 | .UsingTemplate(template, new ViewModel { Name = "LUKE", Numbers = new[] { "1", "2", "3" } }); 123 | 124 | Assert.AreEqual("sup LUKE here is a list 123", email.Data.Body); 125 | } 126 | 127 | // currently not cached as Fluid is so fast, but can be added later 128 | [Test] 129 | public void New_Reuse_Cached_Templates() 130 | { 131 | const string template = "sup {{ Name }} here is a list {% for i in Numbers %}{{ i }}{% endfor %}"; 132 | const string template2 = "sup {{ Name }} this is the second template"; 133 | 134 | for (var i = 0; i < 10; i++) 135 | { 136 | var email = new Email(FromEmail) 137 | .To(ToEmail) 138 | .Subject(Subject) 139 | .UsingTemplate(template, new ViewModel { Name = i.ToString(), Numbers = new[] { "1", "2", "3" } }); 140 | 141 | Assert.AreEqual("sup " + i + " here is a list 123", email.Data.Body); 142 | 143 | var email2 = new Email(FromEmail) 144 | .To(ToEmail) 145 | .Subject(Subject) 146 | .UsingTemplate(template2, new ViewModel { Name = i.ToString() }); 147 | 148 | Assert.AreEqual("sup " + i + " this is the second template", email2.Data.Body); 149 | } 150 | } 151 | 152 | [Test] 153 | public void Should_be_able_to_use_project_layout() 154 | { 155 | SetupRenderer(new PhysicalFileProvider(Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).Directory!.FullName, "EmailTemplates"))); 156 | 157 | const string template = @"{% layout '_layout.liquid' %} 158 | sup {{ Name }} here is a list {% for i in Numbers %}{{ i }}{% endfor %}"; 159 | 160 | var email = new Email(FromEmail) 161 | .To(ToEmail) 162 | .Subject(Subject) 163 | .UsingTemplate(template, new ViewModel{ Name = "LUKE", Numbers = new[] { "1", "2", "3" } }); 164 | 165 | Assert.AreEqual($"

Hello!

{Environment.NewLine}
{Environment.NewLine}sup LUKE here is a list 123
", email.Data.Body); 166 | } 167 | 168 | [Test] 169 | public void Should_be_able_to_use_embedded_layout() 170 | { 171 | SetupRenderer(new EmbeddedFileProvider(typeof(LiquidTests).Assembly, "FluentEmail.Liquid.Tests.EmailTemplates")); 172 | 173 | const string template = @"{% layout '_embedded.liquid' %} 174 | sup {{ Name }} here is a list {% for i in Numbers %}{{ i }}{% endfor %}"; 175 | 176 | var email = new Email(FromEmail) 177 | .To(ToEmail) 178 | .Subject(Subject) 179 | .UsingTemplate(template, new ViewModel{ Name = "LUKE", Numbers = new[] { "1", "2", "3" } }); 180 | 181 | Assert.AreEqual($"

Hello!

{Environment.NewLine}
{Environment.NewLine}sup LUKE here is a list 123
", email.Data.Body); 182 | } 183 | 184 | private class ViewModel 185 | { 186 | public string Name { get; set; } 187 | public string[] Numbers { get; set; } 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /test/FluentEmail.Razor.Tests/FluentEmail.Razor.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Always 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /test/FluentEmail.Razor.Tests/RazorTests.cs: -------------------------------------------------------------------------------- 1 | using FluentEmail.Core; 2 | using NUnit.Framework; 3 | using System; 4 | using System.Dynamic; 5 | using System.IO; 6 | 7 | namespace FluentEmail.Razor.Tests 8 | { 9 | public class RazorTests 10 | { 11 | const string toEmail = "bob@test.com"; 12 | const string fromEmail = "johno@test.com"; 13 | const string subject = "sup dawg"; 14 | 15 | [Test] 16 | public void Anonymous_Model_With_List_Template_Matches() 17 | { 18 | string template = "sup @Model.Name here is a list @foreach(var i in Model.Numbers) { @i }"; 19 | 20 | var email = new Email(fromEmail) 21 | { 22 | Renderer = new RazorRenderer() 23 | } 24 | .To(toEmail) 25 | .Subject(subject) 26 | .UsingTemplate(template, new { Name = "LUKE", Numbers = new string[] { "1", "2", "3" } }); 27 | 28 | Assert.AreEqual("sup LUKE here is a list 123", email.Data.Body); 29 | } 30 | 31 | [Test] 32 | public void Reuse_Cached_Templates() 33 | { 34 | string template = "sup @Model.Name here is a list @foreach(var i in Model.Numbers) { @i }"; 35 | string template2 = "sup @Model.Name this is the second template"; 36 | 37 | for (var i = 0; i < 10; i++) 38 | { 39 | var email = new Email(fromEmail) 40 | { 41 | Renderer = new RazorRenderer() 42 | } 43 | .To(toEmail) 44 | .Subject(subject) 45 | .UsingTemplate(template, new { Name = i, Numbers = new string[] { "1", "2", "3" } }); 46 | 47 | Assert.AreEqual("sup " + i + " here is a list 123", email.Data.Body); 48 | 49 | var email2 = new Email(fromEmail) 50 | { 51 | Renderer = new RazorRenderer() 52 | } 53 | .To(toEmail) 54 | .Subject(subject) 55 | .UsingTemplate(template2, new { Name = i }); 56 | 57 | Assert.AreEqual("sup " + i + " this is the second template", email2.Data.Body); 58 | } 59 | } 60 | 61 | [Test] 62 | public void New_Anonymous_Model_Template_Matches() 63 | { 64 | string template = "sup @Model.Name"; 65 | 66 | var email = new Email(fromEmail) 67 | { 68 | Renderer = new RazorRenderer() 69 | } 70 | .To(toEmail) 71 | .Subject(subject) 72 | .UsingTemplate(template, new { Name = "LUKE" }); 73 | 74 | Assert.AreEqual("sup LUKE", email.Data.Body); 75 | } 76 | 77 | [Test] 78 | public void New_Anonymous_Model_With_List_Template_Matches() 79 | { 80 | string template = "sup @Model.Name here is a list @foreach(var i in Model.Numbers) { @i }"; 81 | 82 | var email = new Email(fromEmail) 83 | { 84 | Renderer = new RazorRenderer() 85 | } 86 | .To(toEmail) 87 | .Subject(subject) 88 | .UsingTemplate(template, new { Name = "LUKE", Numbers = new string[] { "1", "2", "3" } }); 89 | 90 | Assert.AreEqual("sup LUKE here is a list 123", email.Data.Body); 91 | } 92 | 93 | [Test] 94 | public void New_Reuse_Cached_Templates() 95 | { 96 | string template = "sup @Model.Name here is a list @foreach(var i in Model.Numbers) { @i }"; 97 | string template2 = "sup @Model.Name this is the second template"; 98 | 99 | for (var i = 0; i < 10; i++) 100 | { 101 | var email = new Email(fromEmail) 102 | { 103 | Renderer = new RazorRenderer() 104 | } 105 | .To(toEmail) 106 | .Subject(subject) 107 | .UsingTemplate(template, new { Name = i, Numbers = new string[] { "1", "2", "3" } }); 108 | 109 | Assert.AreEqual("sup " + i + " here is a list 123", email.Data.Body); 110 | 111 | var email2 = new Email(fromEmail) 112 | { 113 | Renderer = new RazorRenderer() 114 | } 115 | .To(toEmail) 116 | .Subject(subject) 117 | .UsingTemplate(template2, new { Name = i }); 118 | 119 | Assert.AreEqual("sup " + i + " this is the second template", email2.Data.Body); 120 | } 121 | } 122 | 123 | 124 | [Test] 125 | public void Should_be_able_to_use_project_layout_with_viewbag() 126 | { 127 | var projectRoot = Directory.GetCurrentDirectory(); 128 | Email.DefaultRenderer = new RazorRenderer(projectRoot); 129 | 130 | string template = @" 131 | @{ 132 | Layout = ""./Shared/_Layout.cshtml""; 133 | } 134 | sup @Model.Name here is a list @foreach(var i in Model.Numbers) { @i }"; 135 | 136 | dynamic viewBag = new ExpandoObject(); 137 | viewBag.Title = "Hello!"; 138 | var email = new Email(fromEmail) 139 | { 140 | Renderer = new RazorRenderer() 141 | } 142 | .To(toEmail) 143 | .Subject(subject) 144 | .UsingTemplate(template, new ViewModelWithViewBag{ Name = "LUKE", Numbers = new[] { "1", "2", "3" }, ViewBag = viewBag}); 145 | 146 | Assert.AreEqual($"

Hello!

{Environment.NewLine}
{Environment.NewLine}sup LUKE here is a list 123
", email.Data.Body); 147 | } 148 | 149 | [Test] 150 | public void Should_be_able_to_use_embedded_layout_with_viewbag() 151 | { 152 | string template = @" 153 | @{ 154 | Layout = ""_EmbeddedLayout.cshtml""; 155 | } 156 | sup @Model.Name here is a list @foreach(var i in Model.Numbers) { @i }"; 157 | 158 | dynamic viewBag = new ExpandoObject(); 159 | viewBag.Title = "Hello!"; 160 | var email = new Email(fromEmail) 161 | { 162 | Renderer = new RazorRenderer(typeof(RazorTests)) 163 | } 164 | .To(toEmail) 165 | .Subject(subject) 166 | .UsingTemplate(template, new ViewModelWithViewBag{ Name = "LUKE", Numbers = new[] { "1", "2", "3" }, ViewBag = viewBag}); 167 | 168 | Assert.AreEqual($"

Hello!

{Environment.NewLine}
{Environment.NewLine}sup LUKE here is a list 123
", email.Data.Body); 169 | } 170 | } 171 | 172 | public class ViewModelWithViewBag : IViewBagModel 173 | { 174 | public ExpandoObject ViewBag { get; set;} 175 | public string Name {get;set;} 176 | public string[] Numbers {get;set;} 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /test/FluentEmail.Razor.Tests/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 

@ViewBag.Title

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

@ViewBag.Title

2 |
@RenderBody()
--------------------------------------------------------------------------------