Skip to content

Commit bff321e

Browse files
Support for SMTP OAuth authentication through easier IEmailSenderClient implementation (#17484)
* Implement IEmailSenderClient interface and implementation In an effort to support oauth 2 and other schemes, we extract a emailsenderclient interface, allowing to replace default smtp client with one that fits the usecase, without having to implement all of Umbracos logic that builds the mimemessage * fix test * Documentation * EmailMessageExtensions public, use EmailMessage in interface and impl. * move mimemessage into implementation * revert EmailMessageExtensions back to internal * use StaticServiceProvider to avoid breaking change * Fix test after changing constructor * revert constructor change and add new constructor an obsoletes * Moved a paranthesis so it will build in release-mode
1 parent a8705be commit bff321e

File tree

5 files changed

+101
-29
lines changed

5 files changed

+101
-29
lines changed

src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs

+5
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
using Umbraco.Cms.Infrastructure.HostedServices;
4747
using Umbraco.Cms.Infrastructure.Install;
4848
using Umbraco.Cms.Infrastructure.Mail;
49+
using Umbraco.Cms.Infrastructure.Mail.Interfaces;
4950
using Umbraco.Cms.Infrastructure.Manifest;
5051
using Umbraco.Cms.Infrastructure.Migrations;
5152
using Umbraco.Cms.Infrastructure.Migrations.Install;
@@ -172,14 +173,18 @@ public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builde
172173

173174
builder.Services.AddSingleton<IContentLastChanceFinder, ContentFinderByConfigured404>();
174175

176+
builder.Services.AddTransient<IEmailSenderClient, BasicSmtpEmailSenderClient>();
177+
175178
// replace
176179
builder.Services.AddSingleton<IEmailSender, EmailSender>(
177180
services => new EmailSender(
178181
services.GetRequiredService<ILogger<EmailSender>>(),
179182
services.GetRequiredService<IOptionsMonitor<GlobalSettings>>(),
180183
services.GetRequiredService<IEventAggregator>(),
184+
services.GetRequiredService<IEmailSenderClient>(),
181185
services.GetService<INotificationHandler<SendEmailNotification>>(),
182186
services.GetService<INotificationAsyncHandler<SendEmailNotification>>()));
187+
183188
builder.Services.AddTransient<IUserInviteSender, EmailUserInviteSender>();
184189
builder.Services.AddTransient<IUserForgotPasswordSender, EmailUserForgotPasswordSender>();
185190

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System.Net.Mail;
2+
using Microsoft.Extensions.Options;
3+
using Umbraco.Cms.Core.Configuration.Models;
4+
using Umbraco.Cms.Core.Models.Email;
5+
using Umbraco.Cms.Infrastructure.Extensions;
6+
using Umbraco.Cms.Infrastructure.Mail.Interfaces;
7+
using SecureSocketOptions = MailKit.Security.SecureSocketOptions;
8+
using SmtpClient = MailKit.Net.Smtp.SmtpClient;
9+
10+
namespace Umbraco.Cms.Infrastructure.Mail
11+
{
12+
/// <summary>
13+
/// A basic SMTP email sender client using MailKits SMTP client.
14+
/// </summary>
15+
public class BasicSmtpEmailSenderClient : IEmailSenderClient
16+
{
17+
private readonly GlobalSettings _globalSettings;
18+
public BasicSmtpEmailSenderClient(IOptionsMonitor<GlobalSettings> globalSettings)
19+
{
20+
_globalSettings = globalSettings.CurrentValue;
21+
}
22+
23+
public async Task SendAsync(EmailMessage message)
24+
{
25+
using var client = new SmtpClient();
26+
27+
await client.ConnectAsync(
28+
_globalSettings.Smtp!.Host,
29+
_globalSettings.Smtp.Port,
30+
(SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions);
31+
32+
if (!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Username) &&
33+
!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Password))
34+
{
35+
await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password);
36+
}
37+
38+
var mimeMessage = message.ToMimeMessage(_globalSettings.Smtp!.From);
39+
40+
if (_globalSettings.Smtp.DeliveryMethod == SmtpDeliveryMethod.Network)
41+
{
42+
await client.SendAsync(mimeMessage);
43+
}
44+
else
45+
{
46+
client.Send(mimeMessage);
47+
}
48+
}
49+
}
50+
}

src/Umbraco.Infrastructure/Mail/EmailSender.cs

+27-28
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
// Copyright (c) Umbraco.
22
// See LICENSE for more details.
33

4-
using System.Net.Mail;
54
using MailKit.Net.Smtp;
5+
using Microsoft.Extensions.DependencyInjection;
66
using Microsoft.Extensions.Logging;
77
using Microsoft.Extensions.Options;
88
using MimeKit;
99
using MimeKit.IO;
1010
using Umbraco.Cms.Core.Configuration.Models;
11+
using Umbraco.Cms.Core.DependencyInjection;
1112
using Umbraco.Cms.Core.Events;
1213
using Umbraco.Cms.Core.Mail;
1314
using Umbraco.Cms.Core.Models.Email;
1415
using Umbraco.Cms.Core.Notifications;
1516
using Umbraco.Cms.Infrastructure.Extensions;
16-
using SecureSocketOptions = MailKit.Security.SecureSocketOptions;
17-
using SmtpClient = MailKit.Net.Smtp.SmtpClient;
17+
using Umbraco.Cms.Infrastructure.Mail.Interfaces;
1818

1919
namespace Umbraco.Cms.Infrastructure.Mail;
2020

@@ -28,15 +28,18 @@ public class EmailSender : IEmailSender
2828
private readonly ILogger<EmailSender> _logger;
2929
private readonly bool _notificationHandlerRegistered;
3030
private GlobalSettings _globalSettings;
31+
private readonly IEmailSenderClient _emailSenderClient;
3132

33+
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
3234
public EmailSender(
3335
ILogger<EmailSender> logger,
3436
IOptionsMonitor<GlobalSettings> globalSettings,
3537
IEventAggregator eventAggregator)
36-
: this(logger, globalSettings, eventAggregator, null, null)
38+
: this(logger, globalSettings, eventAggregator,null, null)
3739
{
3840
}
3941

42+
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
4043
public EmailSender(
4144
ILogger<EmailSender> logger,
4245
IOptionsMonitor<GlobalSettings> globalSettings,
@@ -48,6 +51,24 @@ public EmailSender(
4851
_eventAggregator = eventAggregator;
4952
_globalSettings = globalSettings.CurrentValue;
5053
_notificationHandlerRegistered = handler1 is not null || handler2 is not null;
54+
_emailSenderClient = StaticServiceProvider.Instance.GetRequiredService<IEmailSenderClient>();
55+
globalSettings.OnChange(x => _globalSettings = x);
56+
}
57+
58+
[ActivatorUtilitiesConstructor]
59+
public EmailSender(
60+
ILogger<EmailSender> logger,
61+
IOptionsMonitor<GlobalSettings> globalSettings,
62+
IEventAggregator eventAggregator,
63+
IEmailSenderClient emailSenderClient,
64+
INotificationHandler<SendEmailNotification>? handler1,
65+
INotificationAsyncHandler<SendEmailNotification>? handler2)
66+
{
67+
_logger = logger;
68+
_eventAggregator = eventAggregator;
69+
_globalSettings = globalSettings.CurrentValue;
70+
_notificationHandlerRegistered = handler1 is not null || handler2 is not null;
71+
_emailSenderClient = emailSenderClient;
5172
globalSettings.OnChange(x => _globalSettings = x);
5273
}
5374

@@ -152,29 +173,7 @@ private async Task SendAsyncInternal(EmailMessage message, string emailType, boo
152173
while (true);
153174
}
154175

155-
using var client = new SmtpClient();
156-
157-
await client.ConnectAsync(
158-
_globalSettings.Smtp!.Host,
159-
_globalSettings.Smtp.Port,
160-
(SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions);
161-
162-
if (!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Username) &&
163-
!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Password))
164-
{
165-
await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password);
166-
}
167-
168-
var mailMessage = message.ToMimeMessage(_globalSettings.Smtp.From);
169-
if (_globalSettings.Smtp.DeliveryMethod == SmtpDeliveryMethod.Network)
170-
{
171-
await client.SendAsync(mailMessage);
172-
}
173-
else
174-
{
175-
client.Send(mailMessage);
176-
}
177-
178-
await client.DisconnectAsync(true);
176+
await _emailSenderClient.SendAsync(message);
179177
}
178+
180179
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Umbraco.Cms.Core.Models.Email;
2+
3+
namespace Umbraco.Cms.Infrastructure.Mail.Interfaces
4+
{
5+
/// <summary>
6+
/// Client for sending an email from a MimeMessage
7+
/// </summary>
8+
public interface IEmailSenderClient
9+
{
10+
/// <summary>
11+
/// Sends the email message
12+
/// </summary>
13+
/// <param name="message"></param>
14+
/// <returns></returns>
15+
public Task SendAsync(EmailMessage message);
16+
}
17+
}

tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
using Umbraco.Cms.Core.Serialization;
3636
using Umbraco.Cms.Core.Strings;
3737
using Umbraco.Cms.Infrastructure.Mail;
38+
using Umbraco.Cms.Infrastructure.Mail.Interfaces;
3839
using Umbraco.Cms.Infrastructure.Persistence;
3940
using Umbraco.Cms.Infrastructure.Persistence.Mappers;
4041
using Umbraco.Cms.Persistence.SqlServer.Services;
@@ -80,7 +81,7 @@ public static class TestHelper
8081

8182
public static UriUtility UriUtility => s_testHelperInternal.UriUtility;
8283

83-
public static IEmailSender EmailSender { get; } = new EmailSender(new NullLogger<EmailSender>(), new TestOptionsMonitor<GlobalSettings>(new GlobalSettings()), Mock.Of<IEventAggregator>());
84+
public static IEmailSender EmailSender { get; } = new EmailSender(new NullLogger<EmailSender>(), new TestOptionsMonitor<GlobalSettings>(new GlobalSettings()), Mock.Of<IEventAggregator>(), Mock.Of<IEmailSenderClient>(), null,null);
8485

8586
public static ITypeFinder GetTypeFinder() => s_testHelperInternal.GetTypeFinder();
8687

0 commit comments

Comments
 (0)