diff --git a/src/Orchard.Tests/Orchard.Framework.Tests.csproj b/src/Orchard.Tests/Orchard.Framework.Tests.csproj index 0758a2d8b..1b272e1ff 100644 --- a/src/Orchard.Tests/Orchard.Framework.Tests.csproj +++ b/src/Orchard.Tests/Orchard.Framework.Tests.csproj @@ -133,7 +133,6 @@ ..\..\lib\sqlce\System.Data.SqlServerCe.dll True - ..\packages\Microsoft.AspNet.WebApi.Client.5.2.7\lib\net45\System.Net.Http.Formatting.dll diff --git a/src/Orchard.Web/Modules/Orchard.Email/Controllers/EmailAdminController.cs b/src/Orchard.Web/Modules/Orchard.Email/Controllers/EmailAdminController.cs index e5d64f49e..f676bfd5f 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Controllers/EmailAdminController.cs +++ b/src/Orchard.Web/Modules/Orchard.Email/Controllers/EmailAdminController.cs @@ -40,9 +40,9 @@ namespace Orchard.Email.Controllers { smtpSettings.Address = testSettings.From; smtpSettings.Host = testSettings.Host; smtpSettings.Port = testSettings.Port; - smtpSettings.EnableSsl = testSettings.EnableSsl; + smtpSettings.AutoSelectEncryption = testSettings.AutoSelectEncryption; + smtpSettings.EncryptionMethod = testSettings.EncryptionMethod; smtpSettings.RequireCredentials = testSettings.RequireCredentials; - smtpSettings.UseDefaultCredentials = testSettings.UseDefaultCredentials; smtpSettings.UserName = testSettings.UserName; smtpSettings.Password = testSettings.Password; @@ -64,10 +64,10 @@ namespace Orchard.Email.Controllers { return Json(new { error = fakeLogger.Message }); } - return Json(new {status = T("Message sent.").Text}); + return Json(new { status = T("Message sent.").Text }); } catch (Exception e) { - return Json(new {error = e.Message}); + return Json(new { error = e.Message }); } finally { var smtpChannelComponent = _smtpChannel as Component; @@ -97,9 +97,9 @@ namespace Orchard.Email.Controllers { public string ReplyTo { get; set; } public string Host { get; set; } public int Port { get; set; } - public bool EnableSsl { get; set; } + public SmtpEncryptionMethod EncryptionMethod { get; set; } + public bool AutoSelectEncryption { get; set; } public bool RequireCredentials { get; set; } - public bool UseDefaultCredentials { get; set; } public string UserName { get; set; } public string Password { get; set; } public string To { get; set; } diff --git a/src/Orchard.Web/Modules/Orchard.Email/Handlers/SmtpSettingsPartHandler.cs b/src/Orchard.Web/Modules/Orchard.Email/Handlers/SmtpSettingsPartHandler.cs index e1160ad46..be2d20010 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Handlers/SmtpSettingsPartHandler.cs +++ b/src/Orchard.Web/Modules/Orchard.Email/Handlers/SmtpSettingsPartHandler.cs @@ -1,12 +1,12 @@ using System; +using System.Configuration; using System.Text; using Orchard.ContentManagement; -using Orchard.Email.Models; using Orchard.ContentManagement.Handlers; +using Orchard.Email.Models; using Orchard.Localization; using Orchard.Logging; using Orchard.Security; -using System.Configuration; namespace Orchard.Email.Handlers { public class SmtpSettingsPartHandler : ContentHandler { @@ -24,7 +24,8 @@ namespace Orchard.Email.Handlers { OnInitializing((context, part) => { part.Port = 25; part.RequireCredentials = false; - part.EnableSsl = false; + part.AutoSelectEncryption = true; + part.EncryptionMethod = SmtpEncryptionMethod.None; }); } diff --git a/src/Orchard.Web/Modules/Orchard.Email/Migrations.cs b/src/Orchard.Web/Modules/Orchard.Email/Migrations.cs index 0409097b9..259974139 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Migrations.cs +++ b/src/Orchard.Web/Modules/Orchard.Email/Migrations.cs @@ -2,10 +2,6 @@ namespace Orchard.Email { public class Migrations : DataMigrationImpl { - - public int Create() { - - return 1; - } + public int Create() => 1; } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Email/Models/SmtpEncryptionMethod.cs b/src/Orchard.Web/Modules/Orchard.Email/Models/SmtpEncryptionMethod.cs new file mode 100644 index 000000000..64ed03aa4 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Models/SmtpEncryptionMethod.cs @@ -0,0 +1,10 @@ +namespace Orchard.Email.Models { + /// + /// Represents an enumeration for mail encryption methods. + /// + public enum SmtpEncryptionMethod { + None = 0, + SslTls = 1, + StartTls = 2 + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Email/Models/SmtpSettingsPart.cs b/src/Orchard.Web/Modules/Orchard.Email/Models/SmtpSettingsPart.cs index 3eb75d952..b710cf881 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Models/SmtpSettingsPart.cs +++ b/src/Orchard.Web/Modules/Orchard.Email/Models/SmtpSettingsPart.cs @@ -1,7 +1,7 @@ -using System.Configuration; +using System; +using System.Configuration; using System.Net.Configuration; using Orchard.ContentManagement; -using System; using Orchard.ContentManagement.Utilities; namespace Orchard.Email.Models { @@ -31,9 +31,23 @@ namespace Orchard.Email.Models { set { this.Store(x => x.Port, value); } } - public bool EnableSsl { - get { return this.Retrieve(x => x.EnableSsl); } - set { this.Store(x => x.EnableSsl, value); } + [Obsolete($"Use {nameof(AutoSelectEncryption)} and/or {nameof(EncryptionMethod)} instead.")] + public bool EnableSsl => this.Retrieve(x => x.EnableSsl); + + public SmtpEncryptionMethod EncryptionMethod { +#pragma warning disable CS0618 // Type or member is obsolete + // Reading EnableSsl is necessary to keep the correct settings during the upgrade to MailKit. + get { return this.Retrieve(x => x.EncryptionMethod, EnableSsl ? SmtpEncryptionMethod.SslTls : SmtpEncryptionMethod.None); } +#pragma warning restore CS0618 // Type or member is obsolete + set { this.Store(x => x.EncryptionMethod, value); } + } + + public bool AutoSelectEncryption { +#pragma warning disable CS0618 // Type or member is obsolete + // Reading EnableSsl is necessary to keep the correct settings during the upgrade to MailKit. + get { return this.Retrieve(x => x.AutoSelectEncryption, !EnableSsl); } +#pragma warning restore CS0618 // Type or member is obsolete + set { this.Store(x => x.AutoSelectEncryption, value); } } public bool RequireCredentials { @@ -41,11 +55,6 @@ namespace Orchard.Email.Models { set { this.Store(x => x.RequireCredentials, value); } } - public bool UseDefaultCredentials { - get { return this.Retrieve(x => x.UseDefaultCredentials); } - set { this.Store(x => x.UseDefaultCredentials, value); } - } - public string UserName { get { return this.Retrieve(x => x.UserName); } set { this.Store(x => x.UserName, value); } diff --git a/src/Orchard.Web/Modules/Orchard.Email/Orchard.Email.csproj b/src/Orchard.Web/Modules/Orchard.Email/Orchard.Email.csproj index f61f1f1f1..1327b1456 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Orchard.Email.csproj +++ b/src/Orchard.Web/Modules/Orchard.Email/Orchard.Email.csproj @@ -55,6 +55,12 @@ false + + ..\..\..\packages\Portable.BouncyCastle.1.9.0\lib\net40\BouncyCastle.Crypto.dll + + + ..\..\..\packages\MailKit.3.1.1\lib\net48\MailKit.dll + ..\..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.2.0.1\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll @@ -62,14 +68,23 @@ ..\..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + + ..\..\..\packages\MimeKit.3.1.1\lib\net48\MimeKit.dll + ..\..\..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll + + ..\..\..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + False + + + @@ -104,6 +119,7 @@ + @@ -218,4 +234,4 @@ - + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Email/Services/MissingSettingsBanner.cs b/src/Orchard.Web/Modules/Orchard.Email/Services/MissingSettingsBanner.cs index 7e2270261..e80f239e2 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Services/MissingSettingsBanner.cs +++ b/src/Orchard.Web/Modules/Orchard.Email/Services/MissingSettingsBanner.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Net.Mail; using System.Web.Mvc; using Orchard.ContentManagement; using Orchard.Email.Models; @@ -24,8 +23,8 @@ namespace Orchard.Email.Services { if (smtpSettings == null || !smtpSettings.IsValid()) { var urlHelper = new UrlHelper(workContext.HttpContext.Request.RequestContext); - var url = urlHelper.Action("Email", "Admin", new {Area = "Settings"}); - yield return new NotifyEntry {Message = T("The SMTP settings need to be configured.", url), Type = NotifyType.Warning}; + var url = urlHelper.Action("Email", "Admin", new { Area = "Settings" }); + yield return new NotifyEntry { Message = T("The SMTP settings need to be configured.", url), Type = NotifyType.Warning }; } } } diff --git a/src/Orchard.Web/Modules/Orchard.Email/Services/SmtpMessageChannel.cs b/src/Orchard.Web/Modules/Orchard.Email/Services/SmtpMessageChannel.cs index 026cc8ad9..3c6437372 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Services/SmtpMessageChannel.cs +++ b/src/Orchard.Web/Modules/Orchard.Email/Services/SmtpMessageChannel.cs @@ -1,14 +1,17 @@ using System; using System.Collections.Generic; using System.Configuration; -using System.Net; +using System.Linq; using System.Net.Configuration; using System.Net.Mail; using System.Web.Mvc; +using MailKit.Security; +using MimeKit; using Orchard.ContentManagement; using Orchard.DisplayManagement; -using Orchard.Logging; using Orchard.Email.Models; +using Orchard.Logging; +using SmtpClient = MailKit.Net.Smtp.SmtpClient; namespace Orchard.Email.Services { public class SmtpMessageChannel : Component, ISmtpChannel, IDisposable { @@ -64,67 +67,55 @@ namespace Orchard.Email.Services { Content = new MvcHtmlString(emailMessage.Body) })); - var mailMessage = new MailMessage { + var mailMessage = new MimeMessage { Subject = emailMessage.Subject, - Body = _shapeDisplay.Display(template), - IsBodyHtml = true + }; + var mailBodyBuilder = new BodyBuilder { + HtmlBody = _shapeDisplay.Display(template), }; - if (parameters.ContainsKey("Message")) { + if (parameters.TryGetValue("Message", out var possiblyMailMessage) && possiblyMailMessage is MailMessage legacyMessage) { // A full message object is provided by the sender. + if (!String.IsNullOrWhiteSpace(legacyMessage.Subject)) { + mailMessage.Subject = legacyMessage.Subject; + } - var oldMessage = mailMessage; - mailMessage = (MailMessage)parameters["Message"]; - - if (String.IsNullOrWhiteSpace(mailMessage.Subject)) - mailMessage.Subject = oldMessage.Subject; - - if (String.IsNullOrWhiteSpace(mailMessage.Body)) { - mailMessage.Body = oldMessage.Body; - mailMessage.IsBodyHtml = oldMessage.IsBodyHtml; + if (!String.IsNullOrWhiteSpace(legacyMessage.Body)) { + mailBodyBuilder.TextBody = legacyMessage.IsBodyHtml ? null : legacyMessage.Body; + mailBodyBuilder.HtmlBody = legacyMessage.IsBodyHtml ? legacyMessage.Body : null; } } - try { + mailMessage.Body = mailBodyBuilder.ToMessageBody(); - foreach (var recipient in ParseRecipients(emailMessage.Recipients)) { - mailMessage.To.Add(new MailAddress(recipient)); - } + try { + mailMessage.To.AddRange(ParseRecipients(emailMessage.Recipients)); if (!String.IsNullOrWhiteSpace(emailMessage.Cc)) { - foreach (var recipient in ParseRecipients(emailMessage.Cc)) { - mailMessage.CC.Add(new MailAddress(recipient)); - } + mailMessage.Cc.AddRange(ParseRecipients(emailMessage.Cc)); } if (!String.IsNullOrWhiteSpace(emailMessage.Bcc)) { - foreach (var recipient in ParseRecipients(emailMessage.Bcc)) { - mailMessage.Bcc.Add(new MailAddress(recipient)); - } + mailMessage.Bcc.AddRange(ParseRecipients(emailMessage.Bcc)); } + var fromAddress = default(MailboxAddress); if (!String.IsNullOrWhiteSpace(emailMessage.From)) { - mailMessage.From = new MailAddress(emailMessage.From); + fromAddress = MailboxAddress.Parse(emailMessage.From); } else { // Take 'From' address from site settings or web.config. - mailMessage.From = !String.IsNullOrWhiteSpace(_smtpSettings.Address) - ? new MailAddress(_smtpSettings.Address) - : new MailAddress(((SmtpSection)ConfigurationManager.GetSection("system.net/mailSettings/smtp")).From); + fromAddress = !String.IsNullOrWhiteSpace(_smtpSettings.Address) + ? MailboxAddress.Parse(_smtpSettings.Address) + : MailboxAddress.Parse(((SmtpSection)ConfigurationManager.GetSection("system.net/mailSettings/smtp")).From); } + mailMessage.From.Add(fromAddress); + if (!String.IsNullOrWhiteSpace(emailMessage.ReplyTo)) { - foreach (var recipient in ParseRecipients(emailMessage.ReplyTo)) { - mailMessage.ReplyToList.Add(new MailAddress(recipient)); - } - } - if (parameters.ContainsKey("NotifyReadEmail")) { - if (parameters["NotifyReadEmail"] is bool) { - if ((bool)(parameters["NotifyReadEmail"])) { - mailMessage.Headers.Add("Disposition-Notification-To", mailMessage.From.ToString()); - } - } + mailMessage.ReplyTo.AddRange(ParseRecipients(emailMessage.ReplyTo)); } + _smtpClientField.Value.Send(mailMessage); } catch (Exception e) { @@ -133,26 +124,51 @@ namespace Orchard.Email.Services { } private SmtpClient CreateSmtpClient() { + var smtpConfiguration = new { + _smtpSettings.Host, + _smtpSettings.Port, + _smtpSettings.EncryptionMethod, + _smtpSettings.AutoSelectEncryption, + _smtpSettings.RequireCredentials, + _smtpSettings.UserName, + _smtpSettings.Password, + }; // If no properties are set in the dashboard, use the web.config value. if (String.IsNullOrWhiteSpace(_smtpSettings.Host)) { - return new SmtpClient(); + var smtpSection = (SmtpSection)ConfigurationManager.GetSection("system.net/mailSettings/smtp"); + if (smtpSection.DeliveryMethod != SmtpDeliveryMethod.Network) { + throw new NotSupportedException($"Only the {SmtpDeliveryMethod.Network} delivery method is supported, but " + + $"{smtpSection.DeliveryMethod} delivery method is configured. Please check your Web.config."); + } + + smtpConfiguration = new { + smtpSection.Network.Host, + smtpSection.Network.Port, + EncryptionMethod = smtpSection.Network.EnableSsl ? SmtpEncryptionMethod.SslTls : SmtpEncryptionMethod.None, + AutoSelectEncryption = !smtpSection.Network.EnableSsl, + RequireCredentials = smtpSection.Network.DefaultCredentials || !String.IsNullOrWhiteSpace(smtpSection.Network.UserName), + smtpSection.Network.UserName, + smtpSection.Network.Password, + }; } - var smtpClient = new SmtpClient { - UseDefaultCredentials = _smtpSettings.RequireCredentials && _smtpSettings.UseDefaultCredentials - }; - - if (!smtpClient.UseDefaultCredentials && !String.IsNullOrWhiteSpace(_smtpSettings.UserName)) { - smtpClient.Credentials = new NetworkCredential(_smtpSettings.UserName, _smtpSettings.Password); + var secureSocketOptions = SecureSocketOptions.Auto; + if (!smtpConfiguration.AutoSelectEncryption) { + secureSocketOptions = smtpConfiguration.EncryptionMethod switch { + SmtpEncryptionMethod.None => SecureSocketOptions.None, + SmtpEncryptionMethod.SslTls => SecureSocketOptions.SslOnConnect, + SmtpEncryptionMethod.StartTls => SecureSocketOptions.StartTls, + _ => SecureSocketOptions.None, + }; } - if (_smtpSettings.Host != null) { - smtpClient.Host = _smtpSettings.Host; + var smtpClient = new SmtpClient(); + smtpClient.Connect(smtpConfiguration.Host, smtpConfiguration.Port, secureSocketOptions); + + if (smtpConfiguration.RequireCredentials) { + smtpClient.Authenticate(smtpConfiguration.UserName, smtpConfiguration.Password); } - smtpClient.Port = _smtpSettings.Port; - smtpClient.EnableSsl = _smtpSettings.EnableSsl; - smtpClient.DeliveryMethod = SmtpDeliveryMethod.Network; return smtpClient; } @@ -160,8 +176,16 @@ namespace Orchard.Email.Services { return dictionary.ContainsKey(key) ? dictionary[key] as string : null; } - private IEnumerable ParseRecipients(string recipients) { - return recipients.Split(new[] {',', ';'}, StringSplitOptions.RemoveEmptyEntries); + private IEnumerable ParseRecipients(string recipients) { + return recipients.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) + .SelectMany(address => { + if (MailboxAddress.TryParse(address, out var mailboxAddress)) { + return new[] { mailboxAddress }; + } + + Logger.Error("Invalid email address: {0}", address); + return Enumerable.Empty(); + }); } } -} \ No newline at end of file +} diff --git a/src/Orchard.Web/Modules/Orchard.Email/Views/EditorTemplates/Parts/SmtpSettings.cshtml b/src/Orchard.Web/Modules/Orchard.Email/Views/EditorTemplates/Parts/SmtpSettings.cshtml index bc4c237d4..1165d0580 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Views/EditorTemplates/Parts/SmtpSettings.cshtml +++ b/src/Orchard.Web/Modules/Orchard.Email/Views/EditorTemplates/Parts/SmtpSettings.cshtml @@ -1,4 +1,5 @@ -@using System.Net.Mail +@using Orchard.Email.Models +@using System.Net.Mail @model Orchard.Email.Models.SmtpSettingsPart @{ var smtpClient = new SmtpClient(); @@ -20,15 +21,37 @@
- @Html.TextBoxFor(m => m.Port, new { placeholder = smtpClient.Port, @class = "text small" }) + @Html.TextBoxFor(m => m.Port, new { type = "number", placeholder = smtpClient.Port, min = 1, max = 65535 }) @Html.ValidationMessage("Port", "*") @T("The SMTP server port, usually 25.")
- @Html.EditorFor(m => m.EnableSsl) - - @Html.ValidationMessage("EnableSsl", "*") - @T("Check if the SMTP server requires SSL communications.") + + + @Html.ValidationMessage("EncryptionMethod", "*") + @T("The encryption method used when connecting to mail server.") +
+
+ @Html.EditorFor(m => m.AutoSelectEncryption) + + @Html.ValidationMessage("AutoSelectEncryption", "*") + @T("Check to let the system select the encryption method based on port.")
@Html.EditorFor(m => m.RequireCredentials) @@ -37,32 +60,17 @@
-
- @Html.RadioButtonFor(m => m.UseDefaultCredentials, false, new { id = "customCredentialsOption", name = "UseDefaultCredentials" }) - - @Html.ValidationMessage("UseDefaultCredentials", "*") + + @Html.TextBoxFor(m => m.UserName, new { @class = "text medium" }) + @Html.ValidationMessage("UserName", "*") + @T("The username for authentication.")
-
- @Html.RadioButtonFor(m => m.UseDefaultCredentials, true, new { id = "defaultCredentialsOptions", name = "UseDefaultCredentials" }) - - @Html.ValidationMessage("UseDefaultCredentials", "*") - @T("When this option is selected, the aplication pool or host-process identity is used to authenticate with the mail server. ") -
-
- - - @Html.TextBoxFor(m => m.UserName, new { @class = "text" }) - @Html.ValidationMessage("UserName", "*") - @T("The username for authentication.") - - - - @Html.TextBoxFor(m => m.Password, new { type = "password", @class = "text medium" }) - @Html.ValidationMessage("Password", "*") - @T("The password for authentication.") - + + @Html.TextBoxFor(m => m.Password, new { type = "password", @class = "text medium" }) + @Html.ValidationMessage("Password", "*") + @T("The password for authentication.")
@@ -106,9 +114,9 @@ from = $("#@Html.FieldIdFor(m => m.Address)"), host = $("#@Html.FieldIdFor(m => m.Host)"), port = $("#@Html.FieldIdFor(m => m.Port)"), - enableSsl = $("#@Html.FieldIdFor(m => m.EnableSsl)"), + encryptionMethod = $("#@Html.FieldIdFor(m => m.EncryptionMethod)"), + autoSelectEncryption = $("#@Html.FieldIdFor(m => m.AutoSelectEncryption)"), requireCredentials = $("#@Html.FieldIdFor(m => m.RequireCredentials)"), - useDefaultCredentials = $("input[name='@Html.NameFor(m => m.UseDefaultCredentials)']"), userName = $("#@Html.FieldIdFor(m => m.UserName)"), password = $("#@Html.FieldIdFor(m => m.Password)"), to = $("#emailtestto"), @@ -123,9 +131,9 @@ from: from.val(), host: host.val(), port: port.val(), - enableSsl: enableSsl.prop("checked"), + encryptionMethod: encryptionMethod.val(), + autoSelectEncryption: autoSelectEncryption.prop("checked"), requireCredentials: requireCredentials.prop("checked"), - useDefaultCredentials: useDefaultCredentials.filter(':checked').val(), userName: userName.val(), password: password.val(), to: to.val(), diff --git a/src/Orchard.Web/Modules/Orchard.Email/Web.config b/src/Orchard.Web/Modules/Orchard.Email/Web.config index 3cd573478..94bd30067 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Web.config +++ b/src/Orchard.Web/Modules/Orchard.Email/Web.config @@ -29,6 +29,7 @@ + diff --git a/src/Orchard.Web/Modules/Orchard.Email/packages.config b/src/Orchard.Web/Modules/Orchard.Email/packages.config index 63841a016..1c240b877 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/packages.config +++ b/src/Orchard.Web/Modules/Orchard.Email/packages.config @@ -1,9 +1,13 @@  + + - + + + \ No newline at end of file