Skip to content

Commit 2c7423a

Browse files
Implemented MailKit sender
1 parent d682bbc commit 2c7423a

File tree

15 files changed

+446
-6
lines changed

15 files changed

+446
-6
lines changed

.github/workflows/publish.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ jobs:
7373
run: dotnet pack --no-build --configuration Release Src/Senders/FluentEmailer.MailerSend/FluentEmailer.MailerSend.csproj --output .
7474
- name: Pack FluentEmailer.Mailgun
7575
run: dotnet pack --no-build --configuration Release Src/Senders/FluentEmailer.Mailgun/FluentEmailer.Mailgun.csproj --output .
76+
- name: Pack FluentEmailer.MailKit
77+
run: dotnet pack --no-build --configuration Release Src/Senders/FluentEmailer.MailKit/FluentEmailer.MailKit.csproj --output .
7678
- name: Pack FluentEmailer.Mailtrap
7779
run: dotnet pack --no-build --configuration Release Src/Senders/FluentEmailer.Mailtrap/FluentEmailer.Mailtrap.csproj --output .
7880
- name: Pack FluentEmailer.SendGrid

FluentEmailer.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentEmailer.Mailgun", "Sr
4646
EndProject
4747
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentEmailer.Mailtrap", "Src\Senders\FluentEmailer.Mailtrap\FluentEmailer.Mailtrap.csproj", "{C4833810-7991-450C-9B7F-B4CB75BEFF69}"
4848
EndProject
49+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentEmailer.MailKit", "Src\Senders\FluentEmailer.MailKit\FluentEmailer.MailKit.csproj", "{732828DC-AE77-460B-B592-BEB3B4C18473}"
50+
EndProject
4951
Global
5052
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5153
Debug|Any CPU = Debug|Any CPU
@@ -96,6 +98,10 @@ Global
9698
{C4833810-7991-450C-9B7F-B4CB75BEFF69}.Debug|Any CPU.Build.0 = Debug|Any CPU
9799
{C4833810-7991-450C-9B7F-B4CB75BEFF69}.Release|Any CPU.ActiveCfg = Release|Any CPU
98100
{C4833810-7991-450C-9B7F-B4CB75BEFF69}.Release|Any CPU.Build.0 = Release|Any CPU
101+
{732828DC-AE77-460B-B592-BEB3B4C18473}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
102+
{732828DC-AE77-460B-B592-BEB3B4C18473}.Debug|Any CPU.Build.0 = Debug|Any CPU
103+
{732828DC-AE77-460B-B592-BEB3B4C18473}.Release|Any CPU.ActiveCfg = Release|Any CPU
104+
{732828DC-AE77-460B-B592-BEB3B4C18473}.Release|Any CPU.Build.0 = Release|Any CPU
99105
EndGlobalSection
100106
GlobalSection(SolutionProperties) = preSolution
101107
HideSolutionNode = FALSE
@@ -115,6 +121,7 @@ Global
115121
{3E47AF7A-E5E4-48DA-A815-1EBE6A45F5D6} = {EF49EAB1-11B5-473B-9E96-2740A533FF8C}
116122
{78DD9C6E-83FF-4C77-BA0B-13BE0D6393CF} = {EF49EAB1-11B5-473B-9E96-2740A533FF8C}
117123
{C4833810-7991-450C-9B7F-B4CB75BEFF69} = {EF49EAB1-11B5-473B-9E96-2740A533FF8C}
124+
{732828DC-AE77-460B-B592-BEB3B4C18473} = {EF49EAB1-11B5-473B-9E96-2740A533FF8C}
118125
EndGlobalSection
119126
GlobalSection(ExtensibilityGlobals) = postSolution
120127
SolutionGuid = {BC1DA9F2-8A43-4E8C-99FB-B1E38B0C4E2F}

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ This library is based on the excellent [FluentEmail](https://github.com/lukencod
2323

2424
* [FluentEmailer.MailerSend](src/Senders/FluentEmailer.MailerSend) - Send email via [MailerSend](https://www.mailersend.com/)'s REST API.
2525
* [FluentEmailer.Mailgun](src/Senders/FluentEmailer.Mailgun) - Send emails via [Mailgun](https://www.mailgun.com/)'s REST API.
26-
* [FluentEmailer.Mailtrap](src/Senders/FluentEmailer.Mailtrap) - Send emails to Mailtrap. Uses [FluentEmailer.Smtp](src/Senders/FluentEmailer.Smtp) for delivery.
26+
* [FluentEmailer.Mailtrap](src/Senders/FluentEmailer.Mailtrap) - Send emails to [Mailtrap](https://mailtrap.io/). Uses [FluentEmailer.Smtp](src/Senders/FluentEmailer.Smtp) for delivery.
27+
* [FluentEmailer.MailKit](src/Senders/FluentEmailer.MailKit) - Send emails using the [MailKit](https://github.com/jstedfast/MailKit) email library.
2728
* [FluentEmailer.SendGrid](src/Senders/FluentEmailer.SendGrid) - Send email via [SendGrid](https://docs.sendgrid.com/for-developers/sending-email/api-getting-started)'s REST API.
2829

2930

Src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<RepositoryUrl>https://github.com/marcoatribeiro/FluentEmailer</RepositoryUrl>
99
<PackageLicenseExpression>MIT</PackageLicenseExpression>
1010
<PackageIcon>fluentemailer_logo_64x64.png</PackageIcon>
11-
<Version>0.6.0</Version>
11+
<Version>1.0.0-rc1</Version>
1212

1313
<PublishRepositoryUrl>true</PublishRepositoryUrl>
1414
<EmbedUntrackedSources>true</EmbedUntrackedSources>

Src/FluentEmailer.Core/Models/Attachment.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ public sealed class Attachment
99
public string Filename { get; set; } = string.Empty;
1010
public Stream Data { get; set; } = default!;
1111
public string ContentType { get; set; } = string.Empty;
12-
public string ContentId { get; set; } = string.Empty;
12+
public string? ContentId { get; set; }
1313
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net7.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<Description>Send emails via MailKit. The SmtpClient has been deprecated and Microsoft recommends using MailKit instead.</Description>
8+
<Authors>Luke Lowrey;Ben Cull;Matt Rutledge;Github Contributors</Authors>
9+
<PackageTags>$(PackageTags);mailkit</PackageTags>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="MailKit" Version="4.1.0" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<ProjectReference Include="..\..\FluentEmailer.Core\FluentEmailer.Core.csproj" />
18+
</ItemGroup>
19+
20+
</Project>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace Microsoft.Extensions.DependencyInjection;
2+
3+
public static class FluentEmailerMailKitBuilderExtensions
4+
{
5+
public static FluentEmailerServicesBuilder AddMailKitSender(this FluentEmailerServicesBuilder builder, SmtpClientOptions smtpClientOptions)
6+
{
7+
builder.Services.TryAdd(ServiceDescriptor.Scoped<ISender>(_ => new MailKitSender(smtpClientOptions)));
8+
return builder;
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
global using FluentEmailer.Core;
2+
global using FluentEmailer.Core.Extensions;
3+
global using FluentEmailer.Core.Interfaces;
4+
global using FluentEmailer.Core.Models;
5+
global using FluentEmailer.MailKit;
6+
global using MailKit.Net.Smtp;
7+
global using MailKit.Security;
8+
global using Microsoft.Extensions.DependencyInjection.Extensions;
9+
global using MimeKit;
10+
global using System.Text;
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
namespace FluentEmailer.MailKit;
2+
3+
/// <summary>
4+
/// Send emails with the MailKit Library.
5+
/// </summary>
6+
public sealed class MailKitSender : ISender
7+
{
8+
private readonly SmtpClientOptions _smtpClientOptions;
9+
10+
/// <summary>
11+
/// Creates a sender that uses the given SmtpClientOptions when sending with MailKit. Since the client is internal this will dispose of the client.
12+
/// </summary>
13+
/// <param name="smtpClientOptions">The SmtpClientOptions to use to create the MailKit client</param>
14+
public MailKitSender(SmtpClientOptions smtpClientOptions)
15+
{
16+
_smtpClientOptions = smtpClientOptions;
17+
}
18+
19+
/// <summary>
20+
/// Send the specified email.
21+
/// </summary>
22+
/// <returns>A response with any errors and a success boolean.</returns>
23+
/// <param name="email">Email.</param>
24+
/// <param name="token">Cancellation Token.</param>
25+
public SendResponse Send(IFluentEmailer email, CancellationToken token = default)
26+
{
27+
var response = new SendResponse();
28+
var message = CreateMailMessage(email);
29+
30+
if (token.IsCancellationRequested)
31+
{
32+
response.ErrorMessages.Add("Message was cancelled by cancellation token.");
33+
return response;
34+
}
35+
36+
try
37+
{
38+
if (_smtpClientOptions.UsePickupDirectory)
39+
{
40+
SaveToPickupDirectory(message, _smtpClientOptions.MailPickupDirectory).Wait(token);
41+
return response;
42+
}
43+
44+
using var client = new SmtpClient();
45+
if (_smtpClientOptions.SocketOptions.HasValue)
46+
{
47+
client.Connect(
48+
_smtpClientOptions.Server,
49+
_smtpClientOptions.Port,
50+
_smtpClientOptions.SocketOptions.Value,
51+
token);
52+
}
53+
else
54+
{
55+
client.Connect(
56+
_smtpClientOptions.Server,
57+
_smtpClientOptions.Port,
58+
_smtpClientOptions.UseSsl,
59+
token);
60+
}
61+
62+
// Note: only needed if the SMTP server requires authentication
63+
if (_smtpClientOptions.RequiresAuthentication)
64+
client.Authenticate(_smtpClientOptions.User, _smtpClientOptions.Password, token);
65+
66+
client.Send(message, token);
67+
client.Disconnect(true, token);
68+
}
69+
catch (Exception ex)
70+
{
71+
response.ErrorMessages.Add(ex.Message);
72+
}
73+
74+
return response;
75+
}
76+
77+
/// <summary>
78+
/// Send the specified email.
79+
/// </summary>
80+
/// <returns>A response with any errors and a success boolean.</returns>
81+
/// <param name="email">Email.</param>
82+
/// <param name="token">Cancellation Token.</param>
83+
public async Task<SendResponse> SendAsync(IFluentEmailer email, CancellationToken token = default)
84+
{
85+
var response = new SendResponse();
86+
var message = CreateMailMessage(email);
87+
88+
if (token.IsCancellationRequested)
89+
{
90+
response.ErrorMessages.Add("Message was cancelled by cancellation token.");
91+
return response;
92+
}
93+
94+
try
95+
{
96+
if (_smtpClientOptions.UsePickupDirectory)
97+
{
98+
await SaveToPickupDirectory(message, _smtpClientOptions.MailPickupDirectory);
99+
return response;
100+
}
101+
102+
using var client = new SmtpClient();
103+
if (_smtpClientOptions.SocketOptions.HasValue)
104+
{
105+
await client.ConnectAsync(
106+
_smtpClientOptions.Server,
107+
_smtpClientOptions.Port,
108+
_smtpClientOptions.SocketOptions.Value,
109+
token);
110+
}
111+
else
112+
{
113+
await client.ConnectAsync(
114+
_smtpClientOptions.Server,
115+
_smtpClientOptions.Port,
116+
_smtpClientOptions.UseSsl,
117+
token);
118+
}
119+
120+
// Note: only needed if the SMTP server requires authentication
121+
if (_smtpClientOptions.RequiresAuthentication)
122+
await client.AuthenticateAsync(_smtpClientOptions.User, _smtpClientOptions.Password, token);
123+
124+
await client.SendAsync(message, token);
125+
await client.DisconnectAsync(true, token);
126+
}
127+
catch (Exception ex)
128+
{
129+
response.ErrorMessages.Add(ex.Message);
130+
}
131+
132+
return response;
133+
}
134+
135+
/// <summary>
136+
/// Saves email to a pickup directory.
137+
/// </summary>
138+
/// <param name="message">Message to save for pickup.</param>
139+
/// <param name="pickupDirectory">Pickup directory.</param>
140+
private static async Task SaveToPickupDirectory(MimeMessage message, string pickupDirectory)
141+
{
142+
// Note: this will require that you know where the specified pickup directory is.
143+
var path = Path.Combine(pickupDirectory, Guid.NewGuid() + ".eml");
144+
145+
if (File.Exists(path))
146+
return;
147+
148+
try
149+
{
150+
await using var stream = new FileStream(path, FileMode.CreateNew);
151+
await message.WriteToAsync(stream);
152+
}
153+
catch (IOException)
154+
{
155+
// The file may have been created between our File.Exists() check and
156+
// our attempt to create the stream.
157+
throw;
158+
}
159+
}
160+
161+
/// <summary>
162+
/// Create a MimMessage so MailKit can send it
163+
/// </summary>
164+
/// <returns>The mail message.</returns>
165+
/// <param name="email">Email data.</param>
166+
private static MimeMessage CreateMailMessage(IFluentEmailer email)
167+
{
168+
var data = email.Data;
169+
170+
var message = new MimeMessage();
171+
172+
// if for any reason, subject header is not added, add it else update it.
173+
if (!message.Headers.Contains(HeaderId.Subject))
174+
message.Headers.Add(HeaderId.Subject, Encoding.UTF8, data.Subject ?? string.Empty);
175+
else
176+
message.Headers[HeaderId.Subject] = data.Subject ?? string.Empty;
177+
178+
message.Headers.Add(HeaderId.Encoding, Encoding.UTF8.EncodingName);
179+
180+
message.From.Add(new MailboxAddress(data.FromAddress.Name, data.FromAddress.EmailAddress));
181+
182+
var builder = new BodyBuilder();
183+
if (!string.IsNullOrEmpty(data.PlaintextAlternativeBody))
184+
{
185+
builder.TextBody = data.PlaintextAlternativeBody;
186+
builder.HtmlBody = data.Body;
187+
}
188+
else if (!data.IsHtml)
189+
{
190+
builder.TextBody = data.Body;
191+
}
192+
else
193+
{
194+
builder.HtmlBody = data.Body;
195+
}
196+
197+
data.Attachments.ForEach(x =>
198+
{
199+
var attachment = builder.Attachments.Add(x.Filename, x.Data, ContentType.Parse(x.ContentType));
200+
attachment.ContentId = x.ContentId;
201+
});
202+
203+
message.Body = builder.ToMessageBody();
204+
205+
foreach (var header in data.Headers)
206+
message.Headers.Add(header.Key, header.Value);
207+
208+
data.ToAddresses.ForEach(x => message.To.Add(new MailboxAddress(x.Name, x.EmailAddress)));
209+
data.CcAddresses.ForEach(x => message.Cc.Add(new MailboxAddress(x.Name, x.EmailAddress)));
210+
data.BccAddresses.ForEach(x => message.Bcc.Add(new MailboxAddress(x.Name, x.EmailAddress)));
211+
data.ReplyToAddresses.ForEach(x => message.ReplyTo.Add(new MailboxAddress(x.Name, x.EmailAddress)));
212+
213+
message.Priority = data.Priority switch
214+
{
215+
Priority.Low => MessagePriority.NonUrgent,
216+
Priority.Normal => MessagePriority.Normal,
217+
Priority.High => MessagePriority.Urgent,
218+
_ => message.Priority
219+
};
220+
221+
return message;
222+
}
223+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace FluentEmailer.MailKit;
2+
3+
public sealed class SmtpClientOptions
4+
{
5+
public string Server { get; set; } = string.Empty;
6+
public int Port { get; set; } = 25;
7+
public string User { get; set; } = string.Empty;
8+
public string Password { get; set; } = string.Empty;
9+
public bool UseSsl { get; set; } = false;
10+
public bool RequiresAuthentication { get; set; } = false;
11+
public string PreferredEncoding { get; set; } = string.Empty;
12+
public bool UsePickupDirectory { get; set; } = false;
13+
public string MailPickupDirectory { get; set; } = string.Empty;
14+
public SecureSocketOptions? SocketOptions { get; set; }
15+
}

0 commit comments

Comments
 (0)