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

(cherry picked from commit bff321e6f5)
This commit is contained in:
kasparboelkjeldsen
2024-11-19 08:48:20 +01:00
committed by Bjarke Berg
parent 2d69eb66ef
commit 7cc8f844f7
5 changed files with 101 additions and 29 deletions

View File

@@ -46,6 +46,7 @@ using Umbraco.Cms.Infrastructure.HealthChecks;
using Umbraco.Cms.Infrastructure.HostedServices; using Umbraco.Cms.Infrastructure.HostedServices;
using Umbraco.Cms.Infrastructure.Install; using Umbraco.Cms.Infrastructure.Install;
using Umbraco.Cms.Infrastructure.Mail; using Umbraco.Cms.Infrastructure.Mail;
using Umbraco.Cms.Infrastructure.Mail.Interfaces;
using Umbraco.Cms.Infrastructure.Manifest; using Umbraco.Cms.Infrastructure.Manifest;
using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Infrastructure.Migrations;
using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Migrations.Install;
@@ -172,14 +173,18 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddSingleton<IContentLastChanceFinder, ContentFinderByConfigured404>(); builder.Services.AddSingleton<IContentLastChanceFinder, ContentFinderByConfigured404>();
builder.Services.AddTransient<IEmailSenderClient, BasicSmtpEmailSenderClient>();
// replace // replace
builder.Services.AddSingleton<IEmailSender, EmailSender>( builder.Services.AddSingleton<IEmailSender, EmailSender>(
services => new EmailSender( services => new EmailSender(
services.GetRequiredService<ILogger<EmailSender>>(), services.GetRequiredService<ILogger<EmailSender>>(),
services.GetRequiredService<IOptionsMonitor<GlobalSettings>>(), services.GetRequiredService<IOptionsMonitor<GlobalSettings>>(),
services.GetRequiredService<IEventAggregator>(), services.GetRequiredService<IEventAggregator>(),
services.GetRequiredService<IEmailSenderClient>(),
services.GetService<INotificationHandler<SendEmailNotification>>(), services.GetService<INotificationHandler<SendEmailNotification>>(),
services.GetService<INotificationAsyncHandler<SendEmailNotification>>())); services.GetService<INotificationAsyncHandler<SendEmailNotification>>()));
builder.Services.AddTransient<IUserInviteSender, EmailUserInviteSender>(); builder.Services.AddTransient<IUserInviteSender, EmailUserInviteSender>();
builder.Services.AddTransient<IUserForgotPasswordSender, EmailUserForgotPasswordSender>(); builder.Services.AddTransient<IUserForgotPasswordSender, EmailUserForgotPasswordSender>();

View File

@@ -0,0 +1,50 @@
using System.Net.Mail;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models.Email;
using Umbraco.Cms.Infrastructure.Extensions;
using Umbraco.Cms.Infrastructure.Mail.Interfaces;
using SecureSocketOptions = MailKit.Security.SecureSocketOptions;
using SmtpClient = MailKit.Net.Smtp.SmtpClient;
namespace Umbraco.Cms.Infrastructure.Mail
{
/// <summary>
/// A basic SMTP email sender client using MailKits SMTP client.
/// </summary>
public class BasicSmtpEmailSenderClient : IEmailSenderClient
{
private readonly GlobalSettings _globalSettings;
public BasicSmtpEmailSenderClient(IOptionsMonitor<GlobalSettings> globalSettings)
{
_globalSettings = globalSettings.CurrentValue;
}
public async Task SendAsync(EmailMessage message)
{
using var client = new SmtpClient();
await client.ConnectAsync(
_globalSettings.Smtp!.Host,
_globalSettings.Smtp.Port,
(SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions);
if (!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Username) &&
!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Password))
{
await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password);
}
var mimeMessage = message.ToMimeMessage(_globalSettings.Smtp!.From);
if (_globalSettings.Smtp.DeliveryMethod == SmtpDeliveryMethod.Network)
{
await client.SendAsync(mimeMessage);
}
else
{
client.Send(mimeMessage);
}
}
}
}

View File

@@ -1,20 +1,20 @@
// Copyright (c) Umbraco. // Copyright (c) Umbraco.
// See LICENSE for more details. // See LICENSE for more details.
using System.Net.Mail;
using MailKit.Net.Smtp; using MailKit.Net.Smtp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MimeKit; using MimeKit;
using MimeKit.IO; using MimeKit.IO;
using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Mail; using Umbraco.Cms.Core.Mail;
using Umbraco.Cms.Core.Models.Email; using Umbraco.Cms.Core.Models.Email;
using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Infrastructure.Extensions; using Umbraco.Cms.Infrastructure.Extensions;
using SecureSocketOptions = MailKit.Security.SecureSocketOptions; using Umbraco.Cms.Infrastructure.Mail.Interfaces;
using SmtpClient = MailKit.Net.Smtp.SmtpClient;
namespace Umbraco.Cms.Infrastructure.Mail; namespace Umbraco.Cms.Infrastructure.Mail;
@@ -28,15 +28,18 @@ public class EmailSender : IEmailSender
private readonly ILogger<EmailSender> _logger; private readonly ILogger<EmailSender> _logger;
private readonly bool _notificationHandlerRegistered; private readonly bool _notificationHandlerRegistered;
private GlobalSettings _globalSettings; private GlobalSettings _globalSettings;
private readonly IEmailSenderClient _emailSenderClient;
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public EmailSender( public EmailSender(
ILogger<EmailSender> logger, ILogger<EmailSender> logger,
IOptionsMonitor<GlobalSettings> globalSettings, IOptionsMonitor<GlobalSettings> globalSettings,
IEventAggregator eventAggregator) IEventAggregator eventAggregator)
: this(logger, globalSettings, eventAggregator, null, null) : this(logger, globalSettings, eventAggregator,null, null)
{ {
} }
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public EmailSender( public EmailSender(
ILogger<EmailSender> logger, ILogger<EmailSender> logger,
IOptionsMonitor<GlobalSettings> globalSettings, IOptionsMonitor<GlobalSettings> globalSettings,
@@ -48,6 +51,24 @@ public class EmailSender : IEmailSender
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_globalSettings = globalSettings.CurrentValue; _globalSettings = globalSettings.CurrentValue;
_notificationHandlerRegistered = handler1 is not null || handler2 is not null; _notificationHandlerRegistered = handler1 is not null || handler2 is not null;
_emailSenderClient = StaticServiceProvider.Instance.GetRequiredService<IEmailSenderClient>();
globalSettings.OnChange(x => _globalSettings = x);
}
[ActivatorUtilitiesConstructor]
public EmailSender(
ILogger<EmailSender> logger,
IOptionsMonitor<GlobalSettings> globalSettings,
IEventAggregator eventAggregator,
IEmailSenderClient emailSenderClient,
INotificationHandler<SendEmailNotification>? handler1,
INotificationAsyncHandler<SendEmailNotification>? handler2)
{
_logger = logger;
_eventAggregator = eventAggregator;
_globalSettings = globalSettings.CurrentValue;
_notificationHandlerRegistered = handler1 is not null || handler2 is not null;
_emailSenderClient = emailSenderClient;
globalSettings.OnChange(x => _globalSettings = x); globalSettings.OnChange(x => _globalSettings = x);
} }
@@ -152,29 +173,7 @@ public class EmailSender : IEmailSender
while (true); while (true);
} }
using var client = new SmtpClient(); await _emailSenderClient.SendAsync(message);
await client.ConnectAsync(
_globalSettings.Smtp!.Host,
_globalSettings.Smtp.Port,
(SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions);
if (!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Username) &&
!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Password))
{
await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password);
}
var mailMessage = message.ToMimeMessage(_globalSettings.Smtp.From);
if (_globalSettings.Smtp.DeliveryMethod == SmtpDeliveryMethod.Network)
{
await client.SendAsync(mailMessage);
}
else
{
client.Send(mailMessage);
}
await client.DisconnectAsync(true);
} }
} }

View File

@@ -0,0 +1,17 @@
using Umbraco.Cms.Core.Models.Email;
namespace Umbraco.Cms.Infrastructure.Mail.Interfaces
{
/// <summary>
/// Client for sending an email from a MimeMessage
/// </summary>
public interface IEmailSenderClient
{
/// <summary>
/// Sends the email message
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public Task SendAsync(EmailMessage message);
}
}

View File

@@ -35,6 +35,7 @@ using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Mail; using Umbraco.Cms.Infrastructure.Mail;
using Umbraco.Cms.Infrastructure.Mail.Interfaces;
using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.Mappers;
using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Persistence.SqlServer.Services;
@@ -80,7 +81,7 @@ public static class TestHelper
public static UriUtility UriUtility => s_testHelperInternal.UriUtility; public static UriUtility UriUtility => s_testHelperInternal.UriUtility;
public static IEmailSender EmailSender { get; } = new EmailSender(new NullLogger<EmailSender>(), new TestOptionsMonitor<GlobalSettings>(new GlobalSettings()), Mock.Of<IEventAggregator>()); public static IEmailSender EmailSender { get; } = new EmailSender(new NullLogger<EmailSender>(), new TestOptionsMonitor<GlobalSettings>(new GlobalSettings()), Mock.Of<IEventAggregator>(), Mock.Of<IEmailSenderClient>(), null,null);
public static ITypeFinder GetTypeFinder() => s_testHelperInternal.GetTypeFinder(); public static ITypeFinder GetTypeFinder() => s_testHelperInternal.GetTypeFinder();