+ @T["Azure email is configured with appsettings.json."] + @T["See documentation."] +
+ +
+ @(string.IsNullOrEmpty(Model.ConnectionString) ? T["Not Configured"] : T["Configured"])
+ @T["The current tenant will be reloaded when the settings are saved."]
-appsettings.json
) using the OrchardCore_Email_Smtp.DefaultSender
key."] @T["See documentation."]
@T["The current tenant will be reloaded when the settings are saved."]
+ +
@if (!string.IsNullOrEmpty(Model.ConnectionString))
{
@@ -24,7 +24,7 @@
-
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ControllerExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ControllerExtensions.cs
index 323d11492be..b4a0f07a60a 100644
--- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ControllerExtensions.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ControllerExtensions.cs
@@ -9,6 +9,7 @@
using Microsoft.Extensions.Logging;
using OrchardCore.DisplayManagement;
using OrchardCore.Email;
+using OrchardCore.Email.Services;
using OrchardCore.Entities;
using OrchardCore.Modules;
using OrchardCore.Settings;
@@ -23,7 +24,7 @@ internal static class ControllerExtensions
{
internal static async Task SendEmailAsync(this Controller controller, string email, string subject, IShape model)
{
- var smtpService = controller.HttpContext.RequestServices.GetRequiredService();
+ var emailService = controller.HttpContext.RequestServices.GetRequiredService();
var displayHelper = controller.HttpContext.RequestServices.GetRequiredService();
var htmlEncoder = controller.HttpContext.RequestServices.GetRequiredService();
var body = string.Empty;
@@ -43,7 +44,7 @@ internal static async Task SendEmailAsync(this Controller controller, stri
IsHtmlBody = true
};
- var result = await smtpService.SendAsync(message);
+ var result = await emailService.SendAsync(message);
return result.Succeeded;
}
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs
index 7295e12b886..30307b03029 100644
--- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/EmailAuthenticatorController.cs
@@ -14,6 +14,7 @@
using OrchardCore.Admin;
using OrchardCore.DisplayManagement.Notify;
using OrchardCore.Email;
+using OrchardCore.Email.Services;
using OrchardCore.Entities;
using OrchardCore.Liquid;
using OrchardCore.Modules;
@@ -29,7 +30,7 @@ namespace OrchardCore.Users.Controllers;
public class EmailAuthenticatorController : TwoFactorAuthenticationBaseController
{
private readonly IUserService _userService;
- private readonly ISmtpService _smtpService;
+ private readonly IEmailService _emailService;
private readonly ILiquidTemplateManager _liquidTemplateManager;
private readonly HtmlEncoder _htmlEncoder;
@@ -43,7 +44,7 @@ public EmailAuthenticatorController(
INotifier notifier,
IDistributedCache distributedCache,
IUserService userService,
- ISmtpService smtpService,
+ IEmailService emailService,
ILiquidTemplateManager liquidTemplateManager,
HtmlEncoder htmlEncoder,
ITwoFactorAuthenticationHandlerCoordinator twoFactorAuthenticationHandlerCoordinator)
@@ -59,7 +60,7 @@ public EmailAuthenticatorController(
twoFactorOptions)
{
_userService = userService;
- _smtpService = smtpService;
+ _emailService = emailService;
_liquidTemplateManager = liquidTemplateManager;
_htmlEncoder = htmlEncoder;
}
@@ -106,7 +107,7 @@ public async Task RequestCode()
IsHtmlBody = true,
};
- var result = await _smtpService.SendAsync(message);
+ var result = await _emailService.SendAsync(message);
if (!result.Succeeded)
{
@@ -178,7 +179,7 @@ public async Task SendCode()
IsHtmlBody = true,
};
- var result = await _smtpService.SendAsync(message);
+ var result = await _emailService.SendAsync(message);
return Ok(new
{
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Workflows/Activities/RegisterUserTask.cs b/src/OrchardCore.Modules/OrchardCore.Users/Workflows/Activities/RegisterUserTask.cs
index 0400668aeea..b12e49fa3b0 100644
--- a/src/OrchardCore.Modules/OrchardCore.Users/Workflows/Activities/RegisterUserTask.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Users/Workflows/Activities/RegisterUserTask.cs
@@ -9,6 +9,7 @@
using Microsoft.Extensions.Localization;
using OrchardCore.DisplayManagement.ModelBinding;
using OrchardCore.Email;
+using OrchardCore.Email.Services;
using OrchardCore.Users.Models;
using OrchardCore.Users.Services;
using OrchardCore.Workflows.Abstractions.Models;
@@ -141,9 +142,9 @@ public override async Task ExecuteAsync(WorkflowExecuti
Body = body,
IsHtmlBody = true
};
- var smtpService = _httpContextAccessor.HttpContext.RequestServices.GetService();
+ var emailService = _httpContextAccessor.HttpContext.RequestServices.GetService();
- if (smtpService == null)
+ if (emailService == null)
{
var updater = _updateModelAccessor.ModelUpdater;
updater?.ModelState.TryAddModelError("", S["No email service is available"]);
@@ -151,7 +152,7 @@ public override async Task ExecuteAsync(WorkflowExecuti
}
else
{
- var result = await smtpService.SendAsync(message);
+ var result = await emailService.SendAsync(message);
if (!result.Succeeded)
{
var updater = _updateModelAccessor.ModelUpdater;
diff --git a/src/OrchardCore.Themes/TheComingSoonTheme/Recipes/comingsoon.recipe.json b/src/OrchardCore.Themes/TheComingSoonTheme/Recipes/comingsoon.recipe.json
index 6491a21a8d0..5b9d3077522 100644
--- a/src/OrchardCore.Themes/TheComingSoonTheme/Recipes/comingsoon.recipe.json
+++ b/src/OrchardCore.Themes/TheComingSoonTheme/Recipes/comingsoon.recipe.json
@@ -30,7 +30,7 @@
"OrchardCore.Deployment",
"OrchardCore.Diagnostics",
"OrchardCore.DynamicCache",
- "OrchardCore.Email",
+ "OrchardCore.Email.Smtp",
"OrchardCore.ReCaptcha",
"OrchardCore.Features",
"OrchardCore.Flows",
diff --git a/src/OrchardCore/OrchardCore.Application.Cms.Core.Targets/OrchardCore.Application.Cms.Core.Targets.csproj b/src/OrchardCore/OrchardCore.Application.Cms.Core.Targets/OrchardCore.Application.Cms.Core.Targets.csproj
index e733b5e20b0..a95239aebbf 100644
--- a/src/OrchardCore/OrchardCore.Application.Cms.Core.Targets/OrchardCore.Application.Cms.Core.Targets.csproj
+++ b/src/OrchardCore/OrchardCore.Application.Cms.Core.Targets/OrchardCore.Application.Cms.Core.Targets.csproj
@@ -60,6 +60,8 @@
+
+
diff --git a/src/OrchardCore/OrchardCore.Email.Abstractions/EmailSettings.cs b/src/OrchardCore/OrchardCore.Email.Abstractions/EmailSettings.cs
new file mode 100644
index 00000000000..12bb2aa7f31
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Email.Abstractions/EmailSettings.cs
@@ -0,0 +1,16 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace OrchardCore.Email;
+
+///
+/// Represents a settings for an email.
+///
+public class EmailSettings
+{
+ ///
+ /// Gets or sets the default sender mail.
+ ///
+ [Required(AllowEmptyStrings = false)]
+ [EmailAddress]
+ public string DefaultSender { get; set; }
+}
diff --git a/src/OrchardCore/OrchardCore.Email.Abstractions/Events/IEmailServiceEvents.cs b/src/OrchardCore/OrchardCore.Email.Abstractions/Events/IEmailServiceEvents.cs
new file mode 100644
index 00000000000..21026d483a9
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Email.Abstractions/Events/IEmailServiceEvents.cs
@@ -0,0 +1,10 @@
+using System.Threading.Tasks;
+
+namespace OrchardCore.Email.Events;
+
+public interface IEmailServiceEvents
+{
+ Task OnMessageSendingAsync(MailMessage message);
+
+ Task OnMessageSentAsync();
+}
diff --git a/src/OrchardCore/OrchardCore.Email.Abstractions/Extensions/MailMessageExtensions.cs b/src/OrchardCore/OrchardCore.Email.Abstractions/Extensions/MailMessageExtensions.cs
new file mode 100644
index 00000000000..14983abd8c9
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Email.Abstractions/Extensions/MailMessageExtensions.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace OrchardCore.Email;
+
+public static class MailMessageExtensions
+{
+ private static readonly char[] _emailsSeparator = [',', ';'];
+
+ public static MailMessageRecipients GetRecipients(this MailMessage message)
+ {
+ var recipients = new MailMessageRecipients();
+ recipients.To.AddRange(SplitMailMessageRecipients(message.To));
+ recipients.Cc.AddRange(SplitMailMessageRecipients(message.Cc));
+ recipients.Bcc.AddRange(SplitMailMessageRecipients(message.Bcc));
+
+ return recipients;
+ }
+ public static IEnumerable GetSender(this MailMessage message) =>
+ string.IsNullOrWhiteSpace(message.From) ? Enumerable.Empty() : SplitMailMessageRecipients(message.From);
+
+ public static IEnumerable GetReplyTo(this MailMessage message) =>
+ string.IsNullOrWhiteSpace(message.ReplyTo) ? message.GetSender() : SplitMailMessageRecipients(message.ReplyTo);
+
+ private static IEnumerable SplitMailMessageRecipients(string recipients) => recipients
+ ?.Split(_emailsSeparator, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) ?? Enumerable.Empty();
+}
diff --git a/src/OrchardCore/OrchardCore.Email.Abstractions/ISmtpService.cs b/src/OrchardCore/OrchardCore.Email.Abstractions/ISmtpService.cs
deleted file mode 100644
index 5c96d62e933..00000000000
--- a/src/OrchardCore/OrchardCore.Email.Abstractions/ISmtpService.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using System.Threading.Tasks;
-
-namespace OrchardCore.Email
-{
- ///
- /// Represents a contract for SMTP service.
- ///
- public interface ISmtpService
- {
- ///
- /// Sends the specified message to an SMTP server for delivery.
- ///
- /// The message to be sent.
- /// A that holds information about the sent message, for instance if it has sent successfully or if it has failed.
- Task SendAsync(MailMessage message);
- }
-}
diff --git a/src/OrchardCore/OrchardCore.Email.Abstractions/MailMessageRecipients.cs b/src/OrchardCore/OrchardCore.Email.Abstractions/MailMessageRecipients.cs
new file mode 100644
index 00000000000..75b6c47d08f
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Email.Abstractions/MailMessageRecipients.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+
+namespace OrchardCore.Email;
+
+public class MailMessageRecipients
+{
+ public List To { get; } = [];
+
+ public List Cc { get; } = [];
+
+ public List Bcc { get; } = [];
+}
diff --git a/src/OrchardCore/OrchardCore.Email.Abstractions/OrchardCore.Email.Abstractions.csproj b/src/OrchardCore/OrchardCore.Email.Abstractions/OrchardCore.Email.Abstractions.csproj
index f2ed0324f69..ef788feb80c 100644
--- a/src/OrchardCore/OrchardCore.Email.Abstractions/OrchardCore.Email.Abstractions.csproj
+++ b/src/OrchardCore/OrchardCore.Email.Abstractions/OrchardCore.Email.Abstractions.csproj
@@ -14,4 +14,8 @@
+
+
+
+
diff --git a/src/OrchardCore/OrchardCore.Email.Abstractions/SmtpResult.cs b/src/OrchardCore/OrchardCore.Email.Abstractions/Services/EmailResult.cs
similarity index 51%
rename from src/OrchardCore/OrchardCore.Email.Abstractions/SmtpResult.cs
rename to src/OrchardCore/OrchardCore.Email.Abstractions/Services/EmailResult.cs
index 729449d8cf8..948444c0412 100644
--- a/src/OrchardCore/OrchardCore.Email.Abstractions/SmtpResult.cs
+++ b/src/OrchardCore/OrchardCore.Email.Abstractions/Services/EmailResult.cs
@@ -1,37 +1,32 @@
using System.Collections.Generic;
using Microsoft.Extensions.Localization;
-namespace OrchardCore.Email
+namespace OrchardCore.Email.Services
{
///
/// Represents the result of sending an email.
///
- public class SmtpResult
+ public class EmailResult : IEmailResult
{
///
- /// Returns an indicating a successful Smtp operation.
+ /// Returns an indicating a successful email operation.
///
- public static SmtpResult Success { get; } = new SmtpResult { Succeeded = true };
+ public static IEmailResult Success { get; } = new EmailResult() { Succeeded = true };
///
- /// An containing an errors that occurred during the Smtp operation.
+ /// An containing an errors that occurred during the email operation.
///
public IEnumerable Errors { get; protected set; }
- ///
- /// Get or sets the response text from the SMTP server.
- ///
- public string Response { get; set; }
-
///
/// Whether the operation succeeded or not.
///
public bool Succeeded { get; protected set; }
///
- /// Creates an indicating a failed Smtp operation, with a list of errors if applicable.
+ /// Creates an indicating a failed email operation, with a list of errors if applicable.
///
/// An optional array of which caused the operation to fail.
- public static SmtpResult Failed(params LocalizedString[] errors) => new() { Succeeded = false, Errors = errors };
+ public static IEmailResult Failed(params LocalizedString[] errors) => new EmailResult() { Succeeded = false, Errors = errors };
}
}
diff --git a/src/OrchardCore/OrchardCore.Email.Abstractions/Services/IEmailDeliveryService.cs b/src/OrchardCore/OrchardCore.Email.Abstractions/Services/IEmailDeliveryService.cs
new file mode 100644
index 00000000000..6dd7b56b431
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Email.Abstractions/Services/IEmailDeliveryService.cs
@@ -0,0 +1,8 @@
+using System.Threading.Tasks;
+
+namespace OrchardCore.Email.Services;
+
+public interface IEmailDeliveryService
+{
+ Task DeliverAsync(MailMessage message);
+}
diff --git a/src/OrchardCore/OrchardCore.Email.Abstractions/Services/IEmailDeliveryServiceResolver.cs b/src/OrchardCore/OrchardCore.Email.Abstractions/Services/IEmailDeliveryServiceResolver.cs
new file mode 100644
index 00000000000..bb2e46ef6bc
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Email.Abstractions/Services/IEmailDeliveryServiceResolver.cs
@@ -0,0 +1,6 @@
+namespace OrchardCore.Email.Services;
+
+public interface IEmailDeliveryServiceResolver
+{
+ IEmailDeliveryService Resolve(string name);
+}
diff --git a/src/OrchardCore/OrchardCore.Email.Abstractions/Services/IEmailMessageValidator.cs b/src/OrchardCore/OrchardCore.Email.Abstractions/Services/IEmailMessageValidator.cs
new file mode 100644
index 00000000000..8d11b3b539e
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Email.Abstractions/Services/IEmailMessageValidator.cs
@@ -0,0 +1,9 @@
+using Microsoft.Extensions.Localization;
+using System.Collections.Generic;
+
+namespace OrchardCore.Email.Services;
+
+public interface IEmailMessageValidator
+{
+ bool IsValid(MailMessage message, out List errors);
+}
diff --git a/src/OrchardCore/OrchardCore.Email.Abstractions/Services/IEmailResult.cs b/src/OrchardCore/OrchardCore.Email.Abstractions/Services/IEmailResult.cs
new file mode 100644
index 00000000000..35c3530b1cb
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Email.Abstractions/Services/IEmailResult.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using Microsoft.Extensions.Localization;
+
+namespace OrchardCore.Email.Services;
+
+public interface IEmailResult
+{
+ IEnumerable Errors { get; }
+
+ bool Succeeded { get; }
+}
diff --git a/src/OrchardCore/OrchardCore.Email.Abstractions/Services/IEmailService.cs b/src/OrchardCore/OrchardCore.Email.Abstractions/Services/IEmailService.cs
new file mode 100644
index 00000000000..03b20d909ed
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Email.Abstractions/Services/IEmailService.cs
@@ -0,0 +1,17 @@
+using System.Threading.Tasks;
+
+namespace OrchardCore.Email.Services;
+
+///
+/// Represents a contract for email service.
+///
+public interface IEmailService
+{
+ ///
+ /// Sends the specified message in email.
+ ///
+ /// The message to be sent in email.
+ /// The name of the delivery service to send the email. If no name is specified then `IEmailDeliveryServiceResolver` will select the last registered one.
+ /// An that holds information about the message sent, for instance, if it was sent successfully or if it has failed.
+ Task SendAsync(MailMessage message, string deliveryServiceName = null);
+}
diff --git a/src/OrchardCore/OrchardCore.Email.Abstractions/SmtpDeliveryMethod.cs b/src/OrchardCore/OrchardCore.Email.Abstractions/SmtpDeliveryMethod.cs
deleted file mode 100644
index 43b8aed4fc6..00000000000
--- a/src/OrchardCore/OrchardCore.Email.Abstractions/SmtpDeliveryMethod.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace OrchardCore.Email
-{
- ///
- /// Represents an enumeration for the mail delivery methods.
- ///
- public enum SmtpDeliveryMethod
- {
- Network,
- SpecifiedPickupDirectory
- }
-}
diff --git a/src/OrchardCore/OrchardCore.Email.Abstractions/SmtpEncryptionMethod.cs b/src/OrchardCore/OrchardCore.Email.Abstractions/SmtpEncryptionMethod.cs
deleted file mode 100644
index 4458bb4271e..00000000000
--- a/src/OrchardCore/OrchardCore.Email.Abstractions/SmtpEncryptionMethod.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace OrchardCore.Email
-{
- ///
- /// Represents an enumeration for mail encryption methods.
- ///
- public enum SmtpEncryptionMethod
- {
- None = 0,
- SslTls = 1,
- StartTls = 2
- }
-}
diff --git a/src/OrchardCore/OrchardCore.Email.Abstractions/SmtpSettings.cs b/src/OrchardCore/OrchardCore.Email.Abstractions/SmtpSettings.cs
deleted file mode 100644
index 8ada10b03e6..00000000000
--- a/src/OrchardCore/OrchardCore.Email.Abstractions/SmtpSettings.cs
+++ /dev/null
@@ -1,111 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Localization;
-
-namespace OrchardCore.Email
-{
- ///
- /// Represents a settings for SMTP.
- ///
- public class SmtpSettings : IValidatableObject
- {
- ///
- /// Gets or sets the default sender mail.
- ///
- [Required(AllowEmptyStrings = false), EmailAddress]
- public string DefaultSender { get; set; }
-
- ///
- /// Gets or sets the mail delivery method.
- ///
- [Required]
- public SmtpDeliveryMethod DeliveryMethod { get; set; }
-
- ///
- /// Gets or sets the mailbox directory, this used for option.
- ///
- public string PickupDirectoryLocation { get; set; }
-
- ///
- /// Gets or sets the SMTP server/host.
- ///
- public string Host { get; set; }
-
- ///
- /// Gets or sets the SMTP port number. Defaults to 25 .
- ///
- [Range(0, 65535)]
- public int Port { get; set; } = 25;
-
- ///
- /// Gets or sets whether the encryption is automatically selected.
- ///
- public bool AutoSelectEncryption { get; set; }
-
- ///
- /// Gets or sets whether the user credentials is required.
- ///
- public bool RequireCredentials { get; set; }
-
- ///
- /// Gets or sets whether to use the default user credentials.
- ///
- public bool UseDefaultCredentials { get; set; }
-
- ///
- /// Gets or sets the mail encryption method.
- ///
- public SmtpEncryptionMethod EncryptionMethod { get; set; }
-
- ///
- /// Gets or sets the user name.
- ///
- public string UserName { get; set; }
-
- ///
- /// Gets or sets the user password.
- ///
- public string Password { get; set; }
-
- ///
- /// Gets or sets the proxy server.
- ///
- public string ProxyHost { get; set; }
-
- ///
- /// Gets or sets the proxy port number.
- ///
- public int ProxyPort { get; set; }
-
- ///
- /// Gets or sets whether invalid SSL certificates should be ignored.
- ///
- public bool IgnoreInvalidSslCertificate { get; set; }
-
- ///
- public IEnumerable Validate(ValidationContext validationContext)
- {
- var S = validationContext.GetService>();
-
- switch (DeliveryMethod)
- {
- case SmtpDeliveryMethod.Network:
- if (string.IsNullOrEmpty(Host))
- {
- yield return new ValidationResult(S["The {0} field is required.", "Host name"], new[] { nameof(Host) });
- }
- break;
- case SmtpDeliveryMethod.SpecifiedPickupDirectory:
- if (string.IsNullOrEmpty(PickupDirectoryLocation))
- {
- yield return new ValidationResult(S["The {0} field is required.", "Pickup directory location"], new[] { nameof(PickupDirectoryLocation) });
- }
- break;
- default:
- throw new NotSupportedException(S["The '{0}' delivery method is not supported.", DeliveryMethod]);
- }
- }
- }
-}
diff --git a/src/OrchardCore/OrchardCore.Email.Core/EmailConstants.cs b/src/OrchardCore/OrchardCore.Email.Core/EmailConstants.cs
new file mode 100644
index 00000000000..266e3d2ab27
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Email.Core/EmailConstants.cs
@@ -0,0 +1,6 @@
+namespace OrchardCore.Email;
+
+public static class EmailConstants
+{
+ public const string NullEmailDeliveryServiceName = "null";
+}
diff --git a/src/OrchardCore/OrchardCore.Email.Core/Extensions/EmailServiceCollectionExtensions.cs b/src/OrchardCore/OrchardCore.Email.Core/Extensions/EmailServiceCollectionExtensions.cs
new file mode 100644
index 00000000000..75c93caa2a2
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Email.Core/Extensions/EmailServiceCollectionExtensions.cs
@@ -0,0 +1,27 @@
+using Microsoft.Extensions.DependencyInjection;
+using OrchardCore.Email.Core.Services;
+using OrchardCore.Email.Services;
+
+namespace OrchardCore.Email;
+
+public static class EmailServiceCollectionExtensions
+{
+ public static IServiceCollection AddEmailServices(this IServiceCollection services)
+ {
+ services.AddScoped();
+ services.AddEmailDeliveryService(EmailConstants.NullEmailDeliveryServiceName);
+ services.AddScoped();
+ services.AddScoped();
+
+ return services;
+ }
+
+ public static IServiceCollection AddEmailDeliveryService(this IServiceCollection services, string key)
+ where TEmailDeliveryService : class, IEmailDeliveryService
+ {
+ services.AddScoped();
+ services.AddKeyedScoped(key);
+
+ return services;
+ }
+}
diff --git a/src/OrchardCore/OrchardCore.Email.Core/Services/EmailDeliveryServiceDictionary.cs b/src/OrchardCore/OrchardCore.Email.Core/Services/EmailDeliveryServiceDictionary.cs
new file mode 100644
index 00000000000..bdbddddfb4f
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Email.Core/Services/EmailDeliveryServiceDictionary.cs
@@ -0,0 +1,7 @@
+using System.Collections.Generic;
+
+namespace OrchardCore.Email.Services;
+
+public class EmailDeliveryServiceDictionary : Dictionary
+{
+}
diff --git a/src/OrchardCore/OrchardCore.Email.Core/Services/EmailDeliveryServiceResolver.cs b/src/OrchardCore/OrchardCore.Email.Core/Services/EmailDeliveryServiceResolver.cs
new file mode 100644
index 00000000000..7bd169bd143
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Email.Core/Services/EmailDeliveryServiceResolver.cs
@@ -0,0 +1,18 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using OrchardCore.Email.Services;
+
+namespace OrchardCore.Email.Core.Services;
+
+public class EmailDeliveryServiceResolver : IEmailDeliveryServiceResolver
+{
+ private readonly IServiceProvider _serviceProvider;
+
+ public EmailDeliveryServiceResolver(IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ }
+
+ public IEmailDeliveryService Resolve(string name) =>
+ _serviceProvider.GetKeyedService(name) ?? _serviceProvider.GetRequiredService();
+}
diff --git a/src/OrchardCore/OrchardCore.Email.Core/Services/EmailMessageValidator.cs b/src/OrchardCore/OrchardCore.Email.Core/Services/EmailMessageValidator.cs
new file mode 100644
index 00000000000..45d9ba30fda
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Email.Core/Services/EmailMessageValidator.cs
@@ -0,0 +1,68 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Options;
+
+namespace OrchardCore.Email.Services;
+
+public class EmailMessageValidator : IEmailMessageValidator
+{
+ private readonly IEmailAddressValidator _emailAddressValidator;
+ private readonly EmailSettings _emailSettings;
+
+ protected readonly IStringLocalizer S;
+
+ public EmailMessageValidator(IEmailAddressValidator emailAddressValidator,
+ IOptions options,
+ IStringLocalizer stringLocalizer)
+ {
+ _emailAddressValidator = emailAddressValidator;
+ _emailSettings = options.Value;
+ S = stringLocalizer;
+ }
+
+ public bool IsValid(MailMessage message, out List errors)
+ {
+ errors = [];
+ var senderAddress = string.IsNullOrWhiteSpace(message.Sender)
+ ? _emailSettings.DefaultSender
+ : message.Sender;
+
+ if (!string.IsNullOrEmpty(senderAddress))
+ {
+ if (!_emailAddressValidator.Validate(senderAddress))
+ {
+ errors.Add(S["Invalid email address for the sender: '{0}'.", senderAddress]);
+ }
+ }
+
+ errors.AddRange(message.GetSender()
+ .Where(address => !_emailAddressValidator.Validate(address))
+ .Select(address => S["Invalid email address for the sender: '{0}'.", address]));
+
+ var recipients = message.GetRecipients();
+
+ errors.AddRange(recipients.To
+ .Where(address => !_emailAddressValidator.Validate(address))
+ .Select(address => S["Invalid email address for the recipient: '{0}'.", address]));
+
+ errors.AddRange(recipients.Cc
+ .Where(address => !_emailAddressValidator.Validate(address))
+ .Select(address => S["Invalid email address for the recipient: '{0}'.", address]));
+
+ errors.AddRange(recipients.Bcc
+ .Where(address => !_emailAddressValidator.Validate(address))
+ .Select(address => S["Invalid email address for the recipient: '{0}'.", address]));
+
+ errors.AddRange(message.GetReplyTo()
+ .Where(address => !_emailAddressValidator.Validate(address))
+ .Select(address => S["Invalid email address for the recipient: '{0}'.", address]));
+
+ if (recipients.To.Count == 0 && recipients.Cc.Count == 0 && recipients.Bcc.Count == 0)
+ {
+ errors.Add(S["The mail message should have at least one of these headers: To, Cc or Bcc."]);
+ }
+
+ return errors.Count == 0;
+ }
+}
diff --git a/src/OrchardCore/OrchardCore.Email.Core/Services/EmailService.cs b/src/OrchardCore/OrchardCore.Email.Core/Services/EmailService.cs
new file mode 100644
index 00000000000..594e5a2b76e
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Email.Core/Services/EmailService.cs
@@ -0,0 +1,45 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using OrchardCore.Email.Events;
+using OrchardCore.Modules;
+
+namespace OrchardCore.Email.Services;
+
+public class EmailService : IEmailService
+{
+ private readonly IEmailMessageValidator _emailMessageValidator;
+ private readonly IEmailDeliveryServiceResolver _emailDeliveryServiceResolver;
+ private readonly IEnumerable _emailServiceEvents;
+ private readonly ILogger _logger;
+
+ public EmailService(
+ IEmailMessageValidator emailMessageValidator,
+ IEmailDeliveryServiceResolver emailDeliveryServiceResolver,
+ IEnumerable emailServiceEvents,
+ ILogger logger)
+ {
+ _emailMessageValidator = emailMessageValidator;
+ _emailDeliveryServiceResolver = emailDeliveryServiceResolver;
+ _emailServiceEvents = emailServiceEvents;
+ _logger = logger;
+ }
+
+ public async Task SendAsync(MailMessage message, string deliveryServiceName = null)
+ {
+ await _emailServiceEvents.InvokeAsync((e, message) => e.OnMessageSendingAsync(message), message, _logger);
+
+ if (!_emailMessageValidator.IsValid(message, out var errors))
+ {
+ return EmailResult.Failed([.. errors]);
+ }
+
+ var emailDeliveryService = _emailDeliveryServiceResolver.Resolve(deliveryServiceName);
+
+ var result = await emailDeliveryService.DeliverAsync(message);
+
+ await _emailServiceEvents.InvokeAsync((e) => e.OnMessageSentAsync(), _logger);
+
+ return result;
+ }
+}
diff --git a/src/OrchardCore/OrchardCore.Email.Core/Services/NullEmailDeliveryService.cs b/src/OrchardCore/OrchardCore.Email.Core/Services/NullEmailDeliveryService.cs
new file mode 100644
index 00000000000..6908d4ccc67
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Email.Core/Services/NullEmailDeliveryService.cs
@@ -0,0 +1,25 @@
+using System.Threading.Tasks;
+using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Logging;
+
+namespace OrchardCore.Email.Services;
+
+public class NullEmailDeliveryService : IEmailDeliveryService
+{
+ private readonly ILogger _logger;
+
+ protected readonly IStringLocalizer S;
+
+ public NullEmailDeliveryService(ILogger logger, IStringLocalizer stringLocalizer)
+ {
+ _logger = logger;
+ S = stringLocalizer;
+ }
+
+ public async Task DeliverAsync(MailMessage message)
+ {
+ _logger.LogWarning("No email delivery service is configured. Please enable an actual implementation so email can be sent.");
+
+ return await Task.FromResult(EmailResult.Failed(S["Please enable an actual implementation so email can be sent."]));
+ }
+}
diff --git a/src/OrchardCore/OrchardCore.Email.Core/Services/SmtpService.cs b/src/OrchardCore/OrchardCore.Email.Core/Services/SmtpService.cs
deleted file mode 100644
index 147da710ab1..00000000000
--- a/src/OrchardCore/OrchardCore.Email.Core/Services/SmtpService.cs
+++ /dev/null
@@ -1,327 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net.Security;
-using System.Security.Cryptography.X509Certificates;
-using System.Threading;
-using System.Threading.Tasks;
-using MailKit.Net.Proxy;
-using MailKit.Net.Smtp;
-using MailKit.Security;
-using Microsoft.Extensions.Localization;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-using MimeKit;
-
-namespace OrchardCore.Email.Services
-{
- ///
- /// Represents a SMTP service that allows to send emails.
- ///
- public class SmtpService : ISmtpService
- {
- private const string EmailExtension = ".eml";
-
- private static readonly char[] _emailsSeparator = [',', ';'];
-
- private readonly SmtpSettings _options;
- private readonly ILogger _logger;
- protected readonly IStringLocalizer S;
-
- ///
- /// Initializes a new instance of a .
- ///
- /// The .
- /// The .
- /// The .
- public SmtpService(
- IOptions options,
- ILogger logger,
- IStringLocalizer stringLocalizer)
- {
- _options = options.Value;
- _logger = logger;
- S = stringLocalizer;
- }
-
- ///
- /// Sends the specified message to an SMTP server for delivery.
- ///
- /// The message to be sent.
- /// A that holds information about the sent message, for instance if it has sent successfully or if it has failed.
- /// This method allows to send an email without setting if or is provided.
- public async Task SendAsync(MailMessage message)
- {
- if (_options == null)
- {
- return SmtpResult.Failed(S["SMTP settings must be configured before an email can be sent."]);
- }
-
- SmtpResult result;
- var response = default(string);
- try
- {
- // Set the MailMessage.From, to avoid the confusion between _options.DefaultSender (Author) and submitter (Sender)
- var senderAddress = string.IsNullOrWhiteSpace(message.From)
- ? _options.DefaultSender
- : message.From;
-
- if (!string.IsNullOrWhiteSpace(senderAddress))
- {
- message.From = senderAddress;
- }
-
- var errors = new List();
-
- var mimeMessage = FromMailMessage(message, errors);
-
- if (errors.Count > 0)
- {
- return SmtpResult.Failed(errors.ToArray());
- }
-
- if (mimeMessage.To.Count == 0 && mimeMessage.Cc.Count == 0 && mimeMessage.Bcc.Count == 0)
- {
- return SmtpResult.Failed(S["The mail message should have at least one of these headers: To, Cc or Bcc."]);
- }
-
- switch (_options.DeliveryMethod)
- {
- case SmtpDeliveryMethod.Network:
- response = await SendOnlineMessageAsync(mimeMessage);
- break;
- case SmtpDeliveryMethod.SpecifiedPickupDirectory:
- await SendOfflineMessageAsync(mimeMessage, _options.PickupDirectoryLocation);
- break;
- default:
- throw new NotSupportedException($"The '{_options.DeliveryMethod}' delivery method is not supported.");
- }
-
- result = SmtpResult.Success;
- }
- catch (Exception ex)
- {
- result = SmtpResult.Failed(S["An error occurred while sending an email: '{0}'", ex.Message]);
- }
-
- result.Response = response;
-
- return result;
- }
-
- private MimeMessage FromMailMessage(MailMessage message, List errors)
- {
- var submitterAddress = string.IsNullOrWhiteSpace(message.Sender)
- ? _options.DefaultSender
- : message.Sender;
-
- var mimeMessage = new MimeMessage();
-
- if (!string.IsNullOrEmpty(submitterAddress))
- {
- if (MailboxAddress.TryParse(submitterAddress, out var mailBox))
- {
- mimeMessage.Sender = mailBox;
-
- }
- else
- {
- errors.Add(S["Invalid email address: '{0}'", submitterAddress]);
- }
- }
-
- if (!string.IsNullOrWhiteSpace(message.From))
- {
- foreach (var address in message.From.Split(_emailsSeparator, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
- {
- if (MailboxAddress.TryParse(address, out var mailBox))
- {
- mimeMessage.From.Add(mailBox);
- }
- else
- {
- errors.Add(S["Invalid email address: '{0}'", address]);
- }
- }
- }
-
- if (!string.IsNullOrWhiteSpace(message.To))
- {
- foreach (var address in message.To.Split(_emailsSeparator, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
- {
- if (MailboxAddress.TryParse(address, out var mailBox))
- {
- mimeMessage.To.Add(mailBox);
- }
- else
- {
- errors.Add(S["Invalid email address: '{0}'", address]);
- }
- }
- }
-
- if (!string.IsNullOrWhiteSpace(message.Cc))
- {
- foreach (var address in message.Cc.Split(_emailsSeparator, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
- {
- if (MailboxAddress.TryParse(address, out var mailBox))
- {
- mimeMessage.Cc.Add(mailBox);
- }
- else
- {
- errors.Add(S["Invalid email address: '{0}'", address]);
- }
- }
- }
-
- if (!string.IsNullOrWhiteSpace(message.Bcc))
- {
- foreach (var address in message.Bcc.Split(_emailsSeparator, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
- {
- if (MailboxAddress.TryParse(address, out var mailBox))
- {
- mimeMessage.Bcc.Add(mailBox);
- }
- else
- {
- errors.Add(S["Invalid email address: '{0}'", address]);
- }
- }
- }
-
- if (string.IsNullOrWhiteSpace(message.ReplyTo))
- {
- foreach (var address in mimeMessage.From)
- {
- mimeMessage.ReplyTo.Add(address);
- }
- }
- else
- {
- foreach (var address in message.ReplyTo.Split(_emailsSeparator, StringSplitOptions.RemoveEmptyEntries))
- {
- if (MailboxAddress.TryParse(address, out var mailBox))
- {
- mimeMessage.ReplyTo.Add(mailBox);
- }
- else
- {
- errors.Add(S["Invalid email address: '{0}'", address]);
- }
- }
- }
-
- mimeMessage.Subject = message.Subject;
-
- var body = new BodyBuilder();
-
- if (message.IsHtmlBody)
- {
- body.HtmlBody = message.Body;
- }
- else
- {
- body.TextBody = message.Body;
- }
-
- foreach (var attachment in message.Attachments)
- {
- // Stream must not be null, otherwise it would try to get the filesystem path
- if (attachment.Stream != null)
- {
- body.Attachments.Add(attachment.Filename, attachment.Stream);
- }
- }
-
- mimeMessage.Body = body.ToMessageBody();
-
- return mimeMessage;
- }
-
- protected virtual Task OnMessageSendingAsync(SmtpClient client, MimeMessage message) => Task.CompletedTask;
-
- private async Task SendOnlineMessageAsync(MimeMessage message)
- {
- var secureSocketOptions = SecureSocketOptions.Auto;
-
- if (!_options.AutoSelectEncryption)
- {
- secureSocketOptions = _options.EncryptionMethod switch
- {
- SmtpEncryptionMethod.None => SecureSocketOptions.None,
- SmtpEncryptionMethod.SslTls => SecureSocketOptions.SslOnConnect,
- SmtpEncryptionMethod.StartTls => SecureSocketOptions.StartTls,
- _ => SecureSocketOptions.Auto,
- };
- }
-
- using var client = new SmtpClient();
-
- client.ServerCertificateValidationCallback = CertificateValidationCallback;
-
- await OnMessageSendingAsync(client, message);
-
- await client.ConnectAsync(_options.Host, _options.Port, secureSocketOptions);
-
- if (_options.RequireCredentials)
- {
- if (_options.UseDefaultCredentials)
- {
- // There's no notion of 'UseDefaultCredentials' in MailKit, so empty credentials is passed in
- await client.AuthenticateAsync(string.Empty, string.Empty);
- }
- else if (!string.IsNullOrWhiteSpace(_options.UserName))
- {
- await client.AuthenticateAsync(_options.UserName, _options.Password);
- }
- }
-
- if (!string.IsNullOrEmpty(_options.ProxyHost))
- {
- client.ProxyClient = new Socks5Client(_options.ProxyHost, _options.ProxyPort);
- }
-
- var response = await client.SendAsync(message);
-
- await client.DisconnectAsync(true);
-
- return response;
- }
-
- private static Task SendOfflineMessageAsync(MimeMessage message, string pickupDirectory)
- {
- var mailPath = Path.Combine(pickupDirectory, Guid.NewGuid().ToString() + EmailExtension);
- return message.WriteToAsync(mailPath, CancellationToken.None);
- }
-
- private bool CertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
- {
- const string logErrorMessage = "SMTP Server's certificate {CertificateSubject} issued by {CertificateIssuer} " +
- "with thumbprint {CertificateThumbprint} and expiration date {CertificateExpirationDate} " +
- "is considered invalid with {SslPolicyErrors} policy errors";
-
- if (sslPolicyErrors == SslPolicyErrors.None)
- {
- return true;
- }
-
- _logger.LogError(logErrorMessage,
- certificate.Subject,
- certificate.Issuer,
- certificate.GetCertHashString(),
- certificate.GetExpirationDateString(),
- sslPolicyErrors);
-
- if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors) && chain?.ChainStatus != null)
- {
- foreach (var chainStatus in chain.ChainStatus)
- {
- _logger.LogError("Status: {Status} - {StatusInformation}", chainStatus.Status, chainStatus.StatusInformation);
- }
- }
-
- return _options.IgnoreInvalidSslCertificate;
- }
- }
-}
diff --git a/src/OrchardCore/OrchardCore.Notifications.Core/Services/EmailNotificationProvider.cs b/src/OrchardCore/OrchardCore.Notifications.Core/Services/EmailNotificationProvider.cs
index e28d5a85664..4da59a6135d 100644
--- a/src/OrchardCore/OrchardCore.Notifications.Core/Services/EmailNotificationProvider.cs
+++ b/src/OrchardCore/OrchardCore.Notifications.Core/Services/EmailNotificationProvider.cs
@@ -2,20 +2,21 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Localization;
using OrchardCore.Email;
+using OrchardCore.Email.Services;
using OrchardCore.Users.Models;
namespace OrchardCore.Notifications.Services;
public class EmailNotificationProvider : INotificationMethodProvider
{
- private readonly ISmtpService _smtpService;
+ private readonly IEmailService _emailService;
protected readonly IStringLocalizer S;
public EmailNotificationProvider(
- ISmtpService smtpService,
+ IEmailService emailService,
IStringLocalizer stringLocalizer)
{
- _smtpService = smtpService;
+ _emailService = emailService;
S = stringLocalizer;
}
@@ -49,7 +50,7 @@ public async Task TrySendAsync(object notify, INotificationMessage message
mailMessage.IsHtmlBody = false;
}
- var result = await _smtpService.SendAsync(mailMessage);
+ var result = await _emailService.SendAsync(mailMessage);
return result.Succeeded;
}
diff --git a/src/docs/reference/README.md b/src/docs/reference/README.md
index 7dc3372e4b3..a8130f28b4f 100644
--- a/src/docs/reference/README.md
+++ b/src/docs/reference/README.md
@@ -114,7 +114,10 @@ Here's a categorized overview of all built-in Orchard Core features at a glance.
- [Logging Serilog](core/Logging.Serilog/README.md)
- [Mini Profiler](modules/MiniProfiler/README.md)
- [Response Compression](modules/ResponseCompression/README.md)
-- [Email](modules/Email/README.md)
+- Email:
+ - [Email](modules/Email/README.md)
+ - [SMTP Email](modules/Email.Smtp/README.md)
+ - [Azure Email](modules/Email.Azure/README.md)
- [Redis](modules/Redis/README.md)
- [Deployment](modules/Deployment/README.md)
- [Diagnostics](modules/Diagnostics/README.md)
diff --git a/src/docs/reference/modules/Email.Azure/README.md b/src/docs/reference/modules/Email.Azure/README.md
new file mode 100644
index 00000000000..5c6c53a88d8
--- /dev/null
+++ b/src/docs/reference/modules/Email.Azure/README.md
@@ -0,0 +1,34 @@
+# Azure Email (`OrchardCore.Email.Azure`)
+
+This module provides the infrastructure necessary to send emails using [Azure Communication Services Email](https://learn.microsoft.com/en-us/azure/communication-services/concepts/email/email-overview).
+
+## Azure Email Settings
+
+Enabling the `OrchardCore.Email.Azure` module will allow the user to set the following settings:
+
+| Setting | Description |
+| --- | --- |
+| `ConnectionString` | The ACS connection string that will be used to deliver the email.
+| `DefaultSender` | The email of the sender. This will overrides the `DefaultSender` setting in [`OrchardCore.Email`](../Email/README.md). |
+
+## Azure Email Settings Configuration
+
+The `OrchardCore.Email.Azure` module allows the user to use configuration values to override the settings configured from the admin area by calling the `ConfigureAzureEmailSettings()` extension method on `OrchardCoreBuilder` when initializing the app.
+
+The following configuration values can be customized:
+
+```json
+ "OrchardCore_Email_Azure": {
+ "DefaultSender": "",
+ "ConnectionString": "",
+ }
+```
+
+!!! note
+ Configuring `DefaultSender` will override the email settings.
+
+For more information please refer to [Configuration](../../core/Configuration/README.md).
+
+## Video
+
+
diff --git a/src/docs/reference/modules/Email.Smtp/README.md b/src/docs/reference/modules/Email.Smtp/README.md
new file mode 100644
index 00000000000..d4fd6f38cc0
--- /dev/null
+++ b/src/docs/reference/modules/Email.Smtp/README.md
@@ -0,0 +1,64 @@
+# SMTP Email (`OrchardCore.Email.Smtp`)
+
+This module provides the infrastructure necessary to send emails using `SMTP`.
+
+## SMTP Settings
+
+Enabling the `OrchardCore.Email.Smtp` feature will allow the user to set the following settings:
+
+| Setting | Description |
+| --- | --- |
+| `DefaultSender` | The email of the sender. This will override the `DefaultSender` setting in [OrchardCore.Email](../Email/README.md). |
+| `DeliveryMethod` | The method for sending the email, `SmtpDeliveryMethod.Network` (online) or `SmtpDeliveryMethod.SpecifiedPickupDirectory` (offline). |
+| `PickupDirectoryLocation` | The directory location for the mailbox (`SmtpDeliveryMethod.SpecifiedPickupDirectory`). |
+| `Host` | The SMTP server. |
+| `Port` | The SMTP port number. |
+| `AutoSelectEncryption` | Whether the SMTP selects the encryption automatically. |
+| `RequireCredentials` | Whether the SMTP requires the user credentials. |
+| `UseDefaultCredentials` | Whether the SMTP will use the default credentials. |
+| `EncryptionMethod` | The SMTP encryption method `SmtpEncryptionMethod.None`, `SmtpEncryptionMethod.SSLTLS` or `SmtpEncryptionMethodSTARTTLS`. |
+| `UserName` | The username for the sender. |
+| `Password` | The password for the sender. |
+| `ProxyHost` | The proxy server. |
+| `ProxyPort` | The proxy port number. |
+
+!!! note
+ You must configure `ProxyHost` and `ProxyPort` if the SMTP server runs through a proxy server.
+
+## SMTP Email Settings Configuration
+
+The `OrchardCore.Email.Smtp` module allows the user to use configuration values to override the settings configured from the admin area by calling the `ConfigureSmtpEmailSettings()` extension method on `OrchardCoreBuilder` when initializing the app.
+
+The following configuration values can be customized:
+
+```json
+ "OrchardCore_Email_Smtp": {
+ "DefaultSender": "Network",
+ "PickupDirectoryLocation": "",
+ "Host": "localhost",
+ "Port": 25,
+ // Uncomment if the SMTP server runs through a proxy server
+ //"ProxyHost": "proxy.domain.com",
+ //"ProxyPort": 5050,
+ "EncryptionMethod": "SSLTLS",
+ "AutoSelectEncryption": false,
+ "UseDefaultCredentials": false,
+ "RequireCredentials": true,
+ "Username": "",
+ "Password": ""
+ }
+```
+
+For more information please refer to [Configuration](../../core/Configuration/README.md).
+
+!!! note
+ You can use still use the old `OrchardCore_Email` section for backward compatibility, but we encourage every one to use `OrchardCore_Email_Smtp` section instead.
+
+## Credits
+
+### MailKit
+
+
+
+Copyright 2013-2019 Xamarin Inc
+Licensed under the MIT License
diff --git a/src/docs/reference/modules/Email/README.md b/src/docs/reference/modules/Email/README.md
index ca0f7b9e605..ec55a41ca52 100644
--- a/src/docs/reference/modules/Email/README.md
+++ b/src/docs/reference/modules/Email/README.md
@@ -1,29 +1,17 @@
# Email (`OrchardCore.Email`)
-This module provides the infrastructure necessary to send emails using `SMTP`.
+This module facilitates the configuration of email settings and will automatically activate upon demand when utilizing at least one email provider service. For further details, refer to the documentation for the [Azure Email](../Email.Azure/README.md) and [SMTP Email](../Email.Smtp/README.md) modules.
-## SMTP Settings
+## Email Settings
Enabling the `OrchardCore.Email` module will allow the user to set the following settings:
| Setting | Description |
| --- | --- |
-| `DefaultSender` | The email of the sender. |
-| `DeliveryMethod` | The method for sending the email, `SmtpDeliveryMethod.Network` (online) or `SmtpDeliveryMethod.SpecifiedPickupDirectory` (offline). |
-| `PickupDirectoryLocation` | The directory location for the mailbox (`SmtpDeliveryMethod.SpecifiedPickupDirectory`). |
-| `Host` | The SMTP server. |
-| `Port` | The SMTP port number. |
-| `AutoSelectEncryption` | Whether the SMTP select the encryption automatically. |
-| `RequireCredentials` | Whether the SMTP requires the user credentials. |
-| `UseDefaultCredentials` | Whether the SMTP will use the default credentials. |
-| `EncryptionMethod` | The SMTP encryption method `SmtpEncryptionMethod.None`, `SmtpEncryptionMethod.SSLTLS` or `SmtpEncryptionMethodSTARTTLS`. |
-| `UserName` | The username for the sender. |
-| `Password` | The password for the sender. |
-| `ProxyHost` | The proxy server. |
-| `ProxyPort` | The proxy port number. |
+| `DefaultSender` | The email of the sender.
!!! note
- You must configure `ProxyHost` and `ProxyPort` if the SMTP server runs through a proxy server.
+ When [OrchardCore.Email.Azure](../Email.Azure/README.md) or [OrchardCore.Email.Smtp](../Email.Smtp/README.md) is enabled you can override the `DefaultSender` setting from the module-specific settings, otherwise, it will fall back to this setting.
## Email Settings Configuration
@@ -34,29 +22,7 @@ The following configuration values can be customized:
```json
"OrchardCore_Email": {
"DefaultSender": "",
- "DefaultSender": "Network",
- "PickupDirectoryLocation": "",
- "Host": "localhost",
- "Port": 25,
- // Uncomment if SMTP server runs through a proxy server
- //"ProxyHost": "proxy.domain.com",
- //"ProxyPort": 5050,
- "EncryptionMethod": "SSLTLS",
- "AutoSelectEncryption": false,
- "UseDefaultCredentials": false,
- "RequireCredentials": true,
- "Username": "",
- "Password": ""
}
```
For more information please refer to [Configuration](../../core/Configuration/README.md).
-
-## Credits
-
-### MailKit
-
-
-
-Copyright 2013-2019 Xamarin Inc
-Licensed under the MIT License
diff --git a/src/docs/releases/1.9.0.md b/src/docs/releases/1.9.0.md
index 017373d3c90..1951d36502e 100644
--- a/src/docs/releases/1.9.0.md
+++ b/src/docs/releases/1.9.0.md
@@ -4,6 +4,21 @@ Release date: Not yet released
## Breaking Changes
+### Email Module
+
+The `OrchardCore.Email` module now only provides email settings and the basics of the email infrastructure, and can't be enabled in itself. To actually send out e-mails, you need to enable an email provider, like `OrchardCore.Email.Smtp` or the new `OrchardCore.Email.Azure` (see notes below). The new `OrchardCore.Email.Smtp` module provides the same capabilities previously built into the `OrchardCore.Email` module, so for simple SMTP emails, enable that.
+
+Module authors who were using `ISmtpService` service previously should now use `IEmailDeliveryService` instead.
+
+**Migration Process**
+
+- If the email settings have been configured in the site settings:
+ 1. Copy the settings before the upgrade.
+ 2. Upgrade Orchard Core to the new version.
+ 3. Re-configure settings. Until you do this, the site won't be able to send e-mails and the old settings won't be loaded.
+- If the email settings have been configured in the `appsettings.json` or other configuration provider:
+ 1. You can still use the old `OrchardCore_Email` section, but we encourage you to use `OrchardCore_Email_Smtp` section instead.
+
### Drop Newtonsoft.Json support
The utilization of [Newtonsoft.Json](https://www.nuget.org/packages/Newtonsoft.Json) has been discontinued in both **YesSql** and **OrchardCore**. Instead, we have transitioned to utilize `System.Text.Json` due to its enhanced performance capabilities. To ensure compatibility with `System.Text.Json` during the serialization or deserialization of objects, the following steps need to be undertaken:
@@ -64,6 +79,11 @@ Additionally, `Twilio` provider is no longer enabled by default. If you want to
## Change Logs
+### Azure Email Module
+
+Introducing a new "Azure Email" module, designed to send emails using Azure Email Communication Services (ACS). For more info read the [Azure Email](../reference/modules/Email.Azure/README.md) docs.
+
+
### Azure AI Search Module
Introducing a new "Azure AI Search" module, designed to empower you in the administration of Azure AI Search indices. When enabled with the "Search" module, it facilitates frontend full-text search capabilities through Azure AI Search. For more info read the [Azure AI Search](../reference/modules/AzureAISearch/README.md) docs.
@@ -81,6 +101,7 @@ Added new extensions to make registering custom deployment step easier:
The method `Task TriggerEventAsync(string name, IDictionary input = null, string correlationId = null, bool isExclusive = false, bool isAlwaysCorrelated = false)`
was changed to return `Task>` instead.
+
### GraphQL Module
When identifying content types for GraphQL exposure, we identify those without a stereotype to provide you with control over the behavior of stereotyped content types. A new option, `DiscoverableSterotypes`, has been introduced in `GraphQLContentOptions`. This allows you to specify stereotypes that should be discoverable by default.
@@ -138,3 +159,9 @@ public class AdminMenu : INavigationProvider
}
}
```
+
+### Smtp Email Module
+
+The "Smtp Email" module is a replacement for old Email module. For more info read the [Smtp Email](../reference/modules/Email.Smtp/README.md) docs.
+
+You can still use the old email module configuration, but it is recommended to use the new one.
diff --git a/test/OrchardCore.Tests/Email/EmailTests.cs b/test/OrchardCore.Tests/Email/EmailTests.cs
deleted file mode 100644
index d54cb406982..00000000000
--- a/test/OrchardCore.Tests/Email/EmailTests.cs
+++ /dev/null
@@ -1,278 +0,0 @@
-using MimeKit;
-using OrchardCore.Email;
-using OrchardCore.Email.Services;
-
-namespace OrchardCore.Tests.Email
-{
- public class EmailTests
- {
- [Fact]
- public async Task SendEmail_WithToHeader()
- {
- // Arrange
- var message = new MailMessage
- {
- To = "info@oc.com",
- Subject = "Test",
- Body = "Test Message"
- };
-
- // Act
- var content = await SendEmailAsync(message, "Your Name ");
-
- // Assert
- Assert.Contains("From: Your Name ", content);
- }
-
- [Fact]
- public async Task SendEmail_WithCcHeader()
- {
- // Arrange
- var message = new MailMessage
- {
- Cc = "info@oc.com",
- Subject = "Test",
- Body = "Test Message"
- };
-
- // Act
- var content = await SendEmailAsync(message);
-
- // Assert
- Assert.Contains("Cc: info@oc.com", content);
- }
-
- [Fact]
- public async Task SendEmail_WithBccHeader()
- {
- // Arrange
- var message = new MailMessage
- {
- Bcc = "info@oc.com",
- Subject = "Test",
- Body = "Test Message"
- };
-
- // Act
- var content = await SendEmailAsync(message);
-
- // Assert
- Assert.Contains("Bcc: info@oc.com", content);
- }
-
- [Fact]
- public async Task SendEmail_WithDisplayName()
- {
- var message = new MailMessage
- {
- To = "info@oc.com",
- Subject = "Test",
- Body = "Test Message"
- };
-
- await SendEmailAsync(message, "Your Name ");
- }
-
- [Fact]
- public async Task SendEmail_UsesDefaultSender()
- {
- var message = new MailMessage
- {
- To = "info@oc.com",
- Subject = "Test",
- Body = "Test Message"
- };
- var content = await SendEmailAsync(message, "Your Name ");
-
- Assert.Contains("From: Your Name ", content);
- }
-
- [Fact]
- public async Task SendEmail_UsesCustomSender()
- {
- var message = new MailMessage
- {
- To = "info@oc.com",
- Subject = "Test",
- Body = "Test Message",
- From = "My Name ",
- };
- var content = await SendEmailAsync(message, "Your Name ");
-
- Assert.Contains("From: My Name ", content);
- Assert.Contains("Sender: Your Name ", content);
- }
-
- [Fact]
- public async Task SendEmail_UsesCustomAuthorAndSender()
- {
- var message = new MailMessage
- {
- To = "info@oc.com",
- Subject = "Test",
- Body = "Test Message",
- Sender = "Hisham Bin Ateya ",
- };
- var content = await SendEmailAsync(message, "Sebastien Ros ");
-
- Assert.Contains("From: Sebastien Ros ", content);
- Assert.Contains("Sender: Hisham Bin Ateya ", content);
- }
-
- [Fact]
- public async Task SendEmail_UsesMultipleAuthors()
- {
- var message = new MailMessage
- {
- To = "info@oc.com",
- Subject = "Test",
- Body = "Test Message",
- From = "sebastienros@gmail.com,hishamco_2007@hotmail.com"
- };
- var content = await SendEmailAsync(message, "Hisham Bin Ateya ");
-
- Assert.Contains("From: sebastienros@gmail.com, hishamco_2007@hotmail.com", content);
- Assert.Contains("Sender: Hisham Bin Ateya ", content);
- }
-
- [Fact]
- public async Task SendEmail_UsesReplyTo()
- {
- var message = new MailMessage
- {
- To = "Hisham Bin Ateya ",
- Subject = "Test",
- Body = "Test Message",
- From = "Hisham Bin Ateya ",
- ReplyTo = "Hisham Bin Ateya ",
- };
- var content = await SendEmailAsync(message, "Your Name ");
-
- Assert.Contains("From: Hisham Bin Ateya ", content);
- Assert.Contains("Reply-To: Hisham Bin Ateya ", content);
- }
-
- [Fact]
- public async Task ReplyTo_ShouldHaveAuthors_IfNotSet()
- {
- var message = new MailMessage
- {
- To = "info@oc.com",
- Subject = "Test",
- Body = "Test Message",
- From = "Sebastien Ros "
- };
- var content = await SendEmailAsync(message, "Your Name ");
-
- Assert.Contains("From: Sebastien Ros ", content);
- Assert.Contains("Reply-To: Sebastien Ros ", content);
- }
-
- [Theory]
- [InlineData("me ", "me", "mailbox@domain.com")]
- [InlineData("me", "me", "mailbox@domain.com")]
- [InlineData("me ", "me", "mailbox@domain.com")]
- [InlineData("", "", "mailbox@domain.com")]
- [InlineData("mailbox@domain.com", "", "mailbox@domain.com")]
- [InlineData("(comment)mailbox(comment)@(comment)domain.com(me) ", "me", "mailbox@domain.com")]
- [InlineData("Sébastien ", "Sébastien", "sébastien@domain.com")]
- public void MailBoxAddress_ShouldParseEmail(string text, string name, string address)
- {
- Assert.True(MailboxAddress.TryParse(text, out var mailboxAddress));
- Assert.Equal(name, mailboxAddress.Name);
- Assert.Equal(address, mailboxAddress.Address);
- }
-
- [Fact]
- public async Task SendEmail_WithoutToAndCcAndBccHeaders_ShouldThrowsException()
- {
- // Arrange
- var message = new MailMessage
- {
- Subject = "Test",
- Body = "Test Message"
- };
- var settings = new SmtpSettings
- {
- DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory
- };
-
- var smtp = CreateSmtpService(settings);
-
- // Act
- var result = await smtp.SendAsync(message);
-
- // Assert
- Assert.True(result.Errors.Any());
- }
-
- [Fact]
- public async Task SendOfflineEmailHasNoResponse()
- {
- // Arrange
- var message = new MailMessage
- {
- To = "info@oc.com",
- Subject = "Test",
- Body = "Test Message"
- };
- var settings = new SmtpSettings
- {
- DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory
- };
-
- var smtp = CreateSmtpService(settings);
-
- // Act
- var result = await smtp.SendAsync(message);
-
- // Assert
- Assert.Null(result.Response);
- }
-
- private static async Task SendEmailAsync(MailMessage message, string defaultSender = null)
- {
- var pickupDirectoryPath = Path.Combine(Directory.GetCurrentDirectory(), "Email");
-
- if (Directory.Exists(pickupDirectoryPath))
- {
- var directory = new DirectoryInfo(pickupDirectoryPath);
- directory.GetFiles().ToList().ForEach(f => f.Delete());
- }
-
- Directory.CreateDirectory(pickupDirectoryPath);
-
- var settings = new SmtpSettings
- {
- DefaultSender = defaultSender,
- DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory,
- PickupDirectoryLocation = pickupDirectoryPath
- };
- var smtp = CreateSmtpService(settings);
-
- var result = await smtp.SendAsync(message);
-
- Assert.True(result.Succeeded);
-
- var file = new DirectoryInfo(pickupDirectoryPath).GetFiles().FirstOrDefault();
-
- Assert.NotNull(file);
-
- var content = File.ReadAllText(file.FullName);
-
- return content;
- }
-
- private static SmtpService CreateSmtpService(SmtpSettings settings)
- {
- var options = new Mock>();
- options.Setup(o => o.Value).Returns(settings);
-
- var logger = new Mock>();
- var localizer = new Mock>();
- var smtp = new SmtpService(options.Object, logger.Object, localizer.Object);
-
- return smtp;
- }
- }
-}
diff --git a/test/OrchardCore.Tests/Email/NullEmailDeliveryServiceTests.cs b/test/OrchardCore.Tests/Email/NullEmailDeliveryServiceTests.cs
new file mode 100644
index 00000000000..e39ae1a62eb
--- /dev/null
+++ b/test/OrchardCore.Tests/Email/NullEmailDeliveryServiceTests.cs
@@ -0,0 +1,27 @@
+using OrchardCore.Email.Services;
+
+namespace OrchardCore.Email.Tests;
+
+public class NullEmailDeliveryServiceTests
+{
+ [Fact]
+ public async Task SendEmail()
+ {
+ // Arrange
+ var logger = NullLogger.Instance;
+ var localizer = Mock.Of>();
+ var emailDeliveryService = new NullEmailDeliveryService(logger, localizer);
+ var message = new MailMessage
+ {
+ To = "test@orchardcore.net",
+ Subject = "Orchard Core",
+ Body = "This is a test message."
+ };
+
+ // Act
+ var result = await emailDeliveryService.DeliverAsync(message);
+
+ // Assert
+ Assert.False(result.Succeeded);
+ }
+}
diff --git a/test/OrchardCore.Tests/Modules/OrchardCore.Email.Azure/Services/AzureEmailDeliveryServiceTests.cs b/test/OrchardCore.Tests/Modules/OrchardCore.Email.Azure/Services/AzureEmailDeliveryServiceTests.cs
new file mode 100644
index 00000000000..e62539385d8
--- /dev/null
+++ b/test/OrchardCore.Tests/Modules/OrchardCore.Email.Azure/Services/AzureEmailDeliveryServiceTests.cs
@@ -0,0 +1,31 @@
+namespace OrchardCore.Email.Azure.Services.Tests;
+
+public class AzureEmailDeliveryServiceTests
+{
+ [Fact(Skip = "Configure the default sender and connection string for Email Communication Services (ECS) before run this test.")]
+ public async Task SendEmailShouldSucceed()
+ {
+ // Arrange
+ var emailOptions = Options.Create(new AzureEmailSettings
+ {
+ DefaultSender = "<>",
+ ConnectionString = "<>"
+ });
+ var emailDeliveryService = new AzureEmailDeliveryService(
+ emailOptions,
+ Mock.Of>(),
+ Mock.Of>());
+ var message = new MailMessage
+ {
+ To = "test@orchardcore.net",
+ Subject = "Orchard Core",
+ Body = "This is a test message."
+ };
+
+ // Act
+ var result = await emailDeliveryService.DeliverAsync(message);
+
+ // Assert
+ Assert.True(result.Succeeded);
+ }
+}
diff --git a/test/OrchardCore.Tests/Modules/OrchardCore.Email.Smtp/SmtpEmailDeliveryServiceTests.cs b/test/OrchardCore.Tests/Modules/OrchardCore.Email.Smtp/SmtpEmailDeliveryServiceTests.cs
new file mode 100644
index 00000000000..891cc7055c9
--- /dev/null
+++ b/test/OrchardCore.Tests/Modules/OrchardCore.Email.Smtp/SmtpEmailDeliveryServiceTests.cs
@@ -0,0 +1,249 @@
+using MimeKit;
+using OrchardCore.Email;
+using OrchardCore.Email.Smtp;
+using OrchardCore.Email.Smtp.Services;
+
+namespace OrchardCore.Modules.Email.Smtp.Tests;
+
+public class SmtpEmailDeliveryServiceTests
+{
+ [Fact]
+ public async Task SendEmail_WithToHeader()
+ {
+ // Arrange
+ var message = new MailMessage
+ {
+ To = "info@oc.com",
+ Subject = "Test",
+ Body = "Test Message"
+ };
+
+ // Act
+ var content = await SendEmailAsync(message, "Your Name ");
+
+ // Assert
+ Assert.Contains("From: Your Name ", content);
+ }
+
+ [Fact]
+ public async Task SendEmail_WithCcHeader()
+ {
+ // Arrange
+ var message = new MailMessage
+ {
+ Cc = "info@oc.com",
+ Subject = "Test",
+ Body = "Test Message"
+ };
+
+ // Act
+ var content = await SendEmailAsync(message);
+
+ // Assert
+ Assert.Contains("Cc: info@oc.com", content);
+ }
+
+ [Fact]
+ public async Task SendEmail_WithBccHeader()
+ {
+ // Arrange
+ var message = new MailMessage
+ {
+ Bcc = "info@oc.com",
+ Subject = "Test",
+ Body = "Test Message"
+ };
+
+ // Act
+ var content = await SendEmailAsync(message);
+
+ // Assert
+ Assert.Contains("Bcc: info@oc.com", content);
+ }
+
+ [Fact]
+ public async Task SendEmail_WithDisplayName()
+ {
+ var message = new MailMessage
+ {
+ To = "info@oc.com",
+ Subject = "Test",
+ Body = "Test Message"
+ };
+
+ await SendEmailAsync(message, "Your Name ");
+ }
+
+ [Fact]
+ public async Task SendEmail_UsesDefaultSender()
+ {
+ var message = new MailMessage
+ {
+ To = "info@oc.com",
+ Subject = "Test",
+ Body = "Test Message"
+ };
+ var content = await SendEmailAsync(message, "Your Name ");
+
+ Assert.Contains("From: Your Name ", content);
+ }
+
+ [Fact]
+ public async Task SendEmail_UsesCustomSender()
+ {
+ var message = new MailMessage
+ {
+ To = "info@oc.com",
+ Subject = "Test",
+ Body = "Test Message",
+ From = "My Name ",
+ };
+ var content = await SendEmailAsync(message, "Your Name ");
+
+ Assert.Contains("From: My Name ", content);
+ Assert.Contains("Sender: Your Name ", content);
+ }
+
+ [Fact]
+ public async Task SendEmail_UsesCustomAuthorAndSender()
+ {
+ var message = new MailMessage
+ {
+ To = "info@oc.com",
+ Subject = "Test",
+ Body = "Test Message",
+ Sender = "Hisham Bin Ateya ",
+ };
+ var content = await SendEmailAsync(message, "Sebastien Ros ");
+
+ Assert.Contains("From: Sebastien Ros ", content);
+ Assert.Contains("Sender: Hisham Bin Ateya ", content);
+ }
+
+ [Fact]
+ public async Task SendEmail_UsesMultipleAuthors()
+ {
+ var message = new MailMessage
+ {
+ To = "info@oc.com",
+ Subject = "Test",
+ Body = "Test Message",
+ From = "sebastienros@gmail.com,hishamco_2007@hotmail.com"
+ };
+ var content = await SendEmailAsync(message, "Hisham Bin Ateya ");
+
+ Assert.Contains("From: sebastienros@gmail.com, hishamco_2007@hotmail.com", content);
+ Assert.Contains("Sender: Hisham Bin Ateya ", content);
+ }
+
+ [Fact]
+ public async Task SendEmail_UsesReplyTo()
+ {
+ var message = new MailMessage
+ {
+ To = "Hisham Bin Ateya ",
+ Subject = "Test",
+ Body = "Test Message",
+ From = "Hisham Bin Ateya ",
+ ReplyTo = "Hisham Bin Ateya ",
+ };
+ var content = await SendEmailAsync(message, "Your Name ");
+
+ Assert.Contains("From: Hisham Bin Ateya ", content);
+ Assert.Contains("Reply-To: Hisham Bin Ateya ", content);
+ }
+
+ [Fact]
+ public async Task ReplyTo_ShouldHaveAuthors_IfNotSet()
+ {
+ var message = new MailMessage
+ {
+ To = "info@oc.com",
+ Subject = "Test",
+ Body = "Test Message",
+ From = "Sebastien Ros "
+ };
+ var content = await SendEmailAsync(message, "Your Name ");
+
+ Assert.Contains("From: Sebastien Ros ", content);
+ Assert.Contains("Reply-To: Sebastien Ros ", content);
+ }
+
+ [Theory]
+ [InlineData("me ", "me", "mailbox@domain.com")]
+ [InlineData("me", "me", "mailbox@domain.com")]
+ [InlineData("me ", "me", "mailbox@domain.com")]
+ [InlineData("", "", "mailbox@domain.com")]
+ [InlineData("mailbox@domain.com", "", "mailbox@domain.com")]
+ [InlineData("(comment)mailbox(comment)@(comment)domain.com(me) ", "me", "mailbox@domain.com")]
+ [InlineData("Sébastien ", "Sébastien", "sébastien@domain.com")]
+ public void MailBoxAddress_ShouldParseEmail(string text, string name, string address)
+ {
+ Assert.True(MailboxAddress.TryParse(text, out var mailboxAddress));
+ Assert.Equal(name, mailboxAddress.Name);
+ Assert.Equal(address, mailboxAddress.Address);
+ }
+
+ [Fact]
+ public async Task SendOfflineEmailHasNoResponse()
+ {
+ // Arrange
+ var message = new MailMessage
+ {
+ To = "info@oc.com",
+ Subject = "Test",
+ Body = "Test Message"
+ };
+ var settings = new SmtpEmailSettings
+ {
+ DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory
+ };
+
+ var smtp = CreateSmtpEmailDeliveryService(settings);
+
+ // Act
+ var result = await smtp.DeliverAsync(message);
+
+ // Assert
+ Assert.Null((result as SmtpEmailResult).Response);
+ }
+
+ private static async Task SendEmailAsync(MailMessage message, string defaultSender = null)
+ {
+ var pickupDirectoryPath = Path.Combine(Directory.GetCurrentDirectory(), "Email");
+
+ if (Directory.Exists(pickupDirectoryPath))
+ {
+ var directory = new DirectoryInfo(pickupDirectoryPath);
+ directory.GetFiles().ToList().ForEach(f => f.Delete());
+ }
+
+ Directory.CreateDirectory(pickupDirectoryPath);
+
+ var settings = new SmtpEmailSettings
+ {
+ DefaultSender = defaultSender,
+ DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory,
+ PickupDirectoryLocation = pickupDirectoryPath
+ };
+ var smtp = CreateSmtpEmailDeliveryService(settings);
+
+ var result = await smtp.DeliverAsync(message);
+
+ Assert.True(result.Succeeded);
+
+ var file = new DirectoryInfo(pickupDirectoryPath).GetFiles().FirstOrDefault();
+
+ Assert.NotNull(file);
+
+ var content = File.ReadAllText(file.FullName);
+
+ return content;
+ }
+
+ private static SmtpEmailDeliveryService CreateSmtpEmailDeliveryService(SmtpEmailSettings settings) => new(
+ Options.Create(settings),
+ Mock.Of>(),
+ Mock.Of>()
+ );
+}
diff --git a/test/OrchardCore.Tests/Modules/OrchardCore.Email/EmailValidatorTests.cs b/test/OrchardCore.Tests/Modules/OrchardCore.Email/EmailValidatorTests.cs
new file mode 100644
index 00000000000..f75ee373e95
--- /dev/null
+++ b/test/OrchardCore.Tests/Modules/OrchardCore.Email/EmailValidatorTests.cs
@@ -0,0 +1,39 @@
+using OrchardCore.Email;
+using OrchardCore.Email.Services;
+using OrchardCore.Email.Smtp;
+using OrchardCore.Email.Smtp.Services;
+
+namespace OrchardCore.Modules.Email.Smtp.Tests;
+
+public class EmailValidatorTests
+{
+ [Fact]
+ public async Task SendEmail_WithoutToAndCcAndBccHeaders_ShouldReturnErrors()
+ {
+ // Arrange
+ var message = new MailMessage
+ {
+ Subject = "Test",
+ Body = "Test Message"
+ };
+ var settings = new SmtpEmailSettings
+ {
+ DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory
+ };
+
+ var smtp = CreateSmtpEmailDeliveryService(settings);
+
+ // Act
+ var result = await smtp.DeliverAsync(message);
+
+ // Assert
+ Assert.True(result.Errors.Any());
+ Assert.NotEqual(EmailResult.Success, result);
+ }
+
+ private static SmtpEmailDeliveryService CreateSmtpEmailDeliveryService(SmtpEmailSettings settings) => new(
+ Options.Create(settings),
+ Mock.Of>(),
+ Mock.Of>()
+ );
+}
diff --git a/test/OrchardCore.Tests/Modules/OrchardCore.Email/Workflows/EmailTaskTests.cs b/test/OrchardCore.Tests/Modules/OrchardCore.Email/Workflows/EmailTaskTests.cs
index ea181507ea0..f5c002a2c0e 100644
--- a/test/OrchardCore.Tests/Modules/OrchardCore.Email/Workflows/EmailTaskTests.cs
+++ b/test/OrchardCore.Tests/Modules/OrchardCore.Email/Workflows/EmailTaskTests.cs
@@ -1,10 +1,13 @@
using OrchardCore.Email;
+using OrchardCore.Email.Core.Services;
+using OrchardCore.Email.Events;
using OrchardCore.Email.Services;
+using OrchardCore.Email.Smtp;
using OrchardCore.Email.Workflows.Activities;
using OrchardCore.Workflows.Models;
using OrchardCore.Workflows.Services;
-namespace OrchardCore.Tests.Modules.OrchardCore.Email.Workflows
+namespace OrchardCore.Modules.Email.Workflows.Tests
{
public class EmailTaskTests
{
@@ -14,9 +17,19 @@ public class EmailTaskTests
public async Task ExecuteTask_WhenToAndCcAndBccAreNotSet_ShouldFails()
{
// Arrange
- var smtpService = CreateSmtpService(new SmtpSettings());
+ var emailSettingsOptions = Options.Create(new SmtpEmailSettings());
+ var emailMessageValidator = new EmailMessageValidator(
+ new EmailAddressValidator(),
+ emailSettingsOptions,
+ Mock.Of>());
+ var emailDeliveryServiceResolver = new EmailDeliveryServiceResolver(Mock.Of());
+ var emailService = new EmailService(
+ emailMessageValidator,
+ emailDeliveryServiceResolver,
+ Enumerable.Empty(),
+ NullLogger.Instance);
var task = new EmailTask(
- smtpService,
+ emailService,
new SimpleWorkflowExpressionEvaluator(),
Mock.Of>(),
HtmlEncoder.Default)
@@ -43,18 +56,6 @@ public async Task ExecuteTask_WhenToAndCcAndBccAreNotSet_ShouldFails()
Assert.Equal("Failed", result.Outcomes.First());
}
- private static ISmtpService CreateSmtpService(SmtpSettings settings)
- {
- var options = new Mock>();
- var logger = new Mock>();
- var localizer = new Mock>();
- var smtp = new SmtpService(options.Object, logger.Object, localizer.Object);
-
- options.Setup(o => o.Value).Returns(settings);
-
- return smtp;
- }
-
private class SimpleWorkflowExpressionEvaluator : IWorkflowExpressionEvaluator
{
public async Task EvaluateAsync(WorkflowExpression expression, WorkflowExecutionContext workflowContext, TextEncoder encoder)
diff --git a/test/OrchardCore.Tests/OrchardCore.Users/RegistrationControllerTests.cs b/test/OrchardCore.Tests/OrchardCore.Users/RegistrationControllerTests.cs
index de95ccf00f3..9b78f73f92b 100644
--- a/test/OrchardCore.Tests/OrchardCore.Users/RegistrationControllerTests.cs
+++ b/test/OrchardCore.Tests/OrchardCore.Users/RegistrationControllerTests.cs
@@ -1,6 +1,7 @@
using OrchardCore.DisplayManagement;
using OrchardCore.DisplayManagement.Notify;
using OrchardCore.Email;
+using OrchardCore.Email.Services;
using OrchardCore.Settings;
using OrchardCore.Tests.Utilities;
using OrchardCore.Users;
@@ -140,7 +141,11 @@ private static RegistrationController SetupRegistrationController(RegistrationSe
var mockSite = SiteMockHelper.GetSite(registrationSettings);
var mockSiteService = Mock.Of(ss => ss.GetSiteSettingsAsync() == Task.FromResult(mockSite.Object));
- var mockSmtpService = Mock.Of(x => x.SendAsync(It.IsAny()) == Task.FromResult(SmtpResult.Success));
+ var mockEmailService = new Mock