Add FromName and ReplyTo properties to SmtpSettingsPart (#8420)

This commit is contained in:
Aaron Amm 2020-10-16 00:10:26 +07:00 committed by GitHub
parent 70c04a9a5f
commit 1c93e4a501
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 204 additions and 112 deletions

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Web.Hosting;
using System.Web.Mvc;
using Orchard.ContentManagement;
using Orchard.Email.Models;
@ -29,8 +28,7 @@ namespace Orchard.Email.Controllers {
ILogger logger = null;
try {
var fakeLogger = new FakeLogger();
var smtpChannelComponent = _smtpChannel as Component;
if (smtpChannelComponent != null) {
if (_smtpChannel is Component smtpChannelComponent) {
logger = smtpChannelComponent.Logger;
smtpChannelComponent.Logger = fakeLogger;
}
@ -38,7 +36,9 @@ namespace Orchard.Email.Controllers {
// Temporarily update settings so that the test will actually use the specified host, port, etc.
var smtpSettings = _orchardServices.WorkContext.CurrentSite.As<SmtpSettingsPart>();
smtpSettings.Address = testSettings.From;
smtpSettings.FromAddress = testSettings.FromAddress;
smtpSettings.FromName = testSettings.FromName;
smtpSettings.ReplyTo = testSettings.ReplyTo;
smtpSettings.Host = testSettings.Host;
smtpSettings.Port = testSettings.Port;
smtpSettings.EnableSsl = testSettings.EnableSsl;
@ -46,6 +46,7 @@ namespace Orchard.Email.Controllers {
smtpSettings.UseDefaultCredentials = testSettings.UseDefaultCredentials;
smtpSettings.UserName = testSettings.UserName;
smtpSettings.Password = testSettings.Password;
smtpSettings.ListUnsubscribe = testSettings.ListUnsubscribe;
if (!smtpSettings.IsValid()) {
fakeLogger.Error("Invalid settings.");
@ -57,7 +58,7 @@ namespace Orchard.Email.Controllers {
});
}
if (!String.IsNullOrEmpty(fakeLogger.Message)) {
if (!string.IsNullOrEmpty(fakeLogger.Message)) {
return Json(new { error = fakeLogger.Message });
}
@ -67,12 +68,11 @@ namespace Orchard.Email.Controllers {
return Json(new { error = e.Message });
}
finally {
var smtpChannelComponent = _smtpChannel as Component;
if (smtpChannelComponent != null) {
if (_smtpChannel is Component smtpChannelComponent) {
smtpChannelComponent.Logger = logger;
}
// Undo the temporarily changed smtp settings.
// Undo the temporarily changed SMTP settings.
_orchardServices.TransactionManager.Cancel();
}
}
@ -80,17 +80,16 @@ namespace Orchard.Email.Controllers {
private class FakeLogger : ILogger {
public string Message { get; set; }
public bool IsEnabled(LogLevel level) {
return true;
}
public bool IsEnabled(LogLevel level) => true;
public void Log(LogLevel level, Exception exception, string format, params object[] args) {
public void Log(LogLevel level, Exception exception, string format, params object[] args) =>
Message = exception == null ? format : exception.Message;
}
}
public class TestSmtpSettings {
public string From { get; set; }
public string FromAddress { get; set; }
public string FromName { get; set; }
public string ReplyTo { get; set; }
public string Host { get; set; }
public int Port { get; set; }
public bool EnableSsl { get; set; }
@ -99,6 +98,7 @@ namespace Orchard.Email.Controllers {
public string UserName { get; set; }
public string Password { get; set; }
public string To { get; set; }
public string ListUnsubscribe { get; set; }
}
}
}

View File

@ -1,11 +1,39 @@
using Orchard.Data.Migration;
using System.Linq;
using System.Xml;
using Orchard.ContentManagement;
using Orchard.Data.Migration;
using Orchard.Email.Models;
namespace Orchard.Email {
public class Migrations : DataMigrationImpl {
private readonly IContentManager _contentManager;
public int Create() {
public Migrations(IContentManager contentManager) => _contentManager = contentManager;
return 1;
// The first migration without any content should not exist but it has been deployed so we need to keep it.
public int Create() => 1;
public int UpdateFrom1() {
// Migrate existing SmtpSettingPart.Address because we rename it to FromAddress.
var siteSettingsItem = _contentManager.Query(contentTypeNames: "Site")
.Slice(1)
.SingleOrDefault();
var siteSettingsRecord = siteSettingsItem?.Record;
if (siteSettingsRecord != null) {
var xmlDoc = new XmlDocument();
xmlDoc.LoadXml(siteSettingsRecord.Data);
var smtpSettingNode = xmlDoc.SelectSingleNode("//SmtpSettingsPart");
if (smtpSettingNode != null) {
var smtpSettingsPart = siteSettingsItem.As<SmtpSettingsPart>();
smtpSettingsPart.FromAddress = smtpSettingNode.Attributes["Address"]?.Value;
}
}
return 2;
}
}
}
}

View File

@ -6,7 +6,8 @@ namespace Orchard.Email.Models {
public string Body { get; set; }
public string Recipients { get; set; }
public string ReplyTo { get; set; }
public string From { get; set; }
public string FromAddress { get; set; }
public string FromName { get; set; }
public string Bcc { get; set; }
public string Cc { get; set; }
/// <summary>

View File

@ -1,72 +1,90 @@
using System.Configuration;
using System.Net.Configuration;
using Orchard.ContentManagement;
using System;
using Orchard.ContentManagement.Utilities;
namespace Orchard.Email.Models {
public class SmtpSettingsPart : ContentPart {
private readonly ComputedField<string> _password = new ComputedField<string>();
public ComputedField<string> PasswordField {
get { return _password; }
public ComputedField<string> PasswordField => _password;
public string FromAddress {
get => this.Retrieve(x => x.FromAddress);
set => this.Store(x => x.FromAddress, value);
}
public string Address {
get { return this.Retrieve(x => x.Address); }
set { this.Store(x => x.Address, value); }
public string FromName {
get => this.Retrieve(x => x.FromName);
set => this.Store(x => x.FromName, value);
}
public string ReplyTo {
get => this.Retrieve(x => x.ReplyTo);
set => this.Store(x => x.ReplyTo, value);
}
private readonly LazyField<string> _addressPlaceholder = new LazyField<string>();
internal LazyField<string> AddressPlaceholderField { get { return _addressPlaceholder; } }
public string AddressPlaceholder { get { return _addressPlaceholder.Value; } }
internal LazyField<string> AddressPlaceholderField => _addressPlaceholder;
public string AddressPlaceholder => _addressPlaceholder.Value;
public string Host {
get { return this.Retrieve(x => x.Host); }
set { this.Store(x => x.Host, value); }
get => this.Retrieve(x => x.Host);
set => this.Store(x => x.Host, value);
}
public int Port {
get { return this.Retrieve(x => x.Port, 25); }
set { this.Store(x => x.Port, value); }
get => this.Retrieve(x => x.Port, 25);
set => this.Store(x => x.Port, value);
}
public bool EnableSsl {
get { return this.Retrieve(x => x.EnableSsl); }
set { this.Store(x => x.EnableSsl, value); }
get => this.Retrieve(x => x.EnableSsl);
set => this.Store(x => x.EnableSsl, value);
}
public bool RequireCredentials {
get { return this.Retrieve(x => x.RequireCredentials); }
set { this.Store(x => x.RequireCredentials, value); }
get => this.Retrieve(x => x.RequireCredentials);
set => this.Store(x => x.RequireCredentials, value);
}
public bool UseDefaultCredentials {
get { return this.Retrieve(x => x.UseDefaultCredentials); }
set { this.Store(x => x.UseDefaultCredentials, value); }
get => 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); }
get => this.Retrieve(x => x.UserName);
set => this.Store(x => x.UserName, value);
}
public string Password {
get { return _password.Value; }
set { _password.Value = value; }
get => _password.Value;
set => _password.Value = value;
}
// Hotmail only supports the mailto:link. When a user clicks on the 'unsubscribe' option in Hotmail.
// Hotmail tries to read the mailto:link in the List-Unsubscribe header.
// If the mailto:link is missing, it moves all the messages to the Junk folder.
// The mailto:link is supported by Gmail, Hotmail, Yahoo, AOL, ATT, Time Warner and Comcast;
// European ISPs such as GMX, Libero, Ziggo, Orange, BTInternet; Russian ISPs such as mail.ru and Yandex;
// and the Chinese domains qq.com, naver.com etc. So most ISPs support (and prefer) mailto:link.
public string ListUnsubscribe {
get => this.Retrieve(x => x.ListUnsubscribe);
set => this.Store(x => x.ListUnsubscribe, value);
}
public bool IsValid() {
var section = (SmtpSection)ConfigurationManager.GetSection("system.net/mailSettings/smtp");
if (section != null && !String.IsNullOrWhiteSpace(section.Network.Host)) {
if (section != null && !string.IsNullOrWhiteSpace(section.Network.Host)) {
return true;
}
if (String.IsNullOrWhiteSpace(Address)) {
if (string.IsNullOrWhiteSpace(FromAddress)) {
return false;
}
if (!String.IsNullOrWhiteSpace(Host) && Port == 0) {
if (!string.IsNullOrWhiteSpace(Host) && Port == 0) {
return false;
}

View File

@ -69,6 +69,9 @@
<Private>False</Private>
</Reference>
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.ValueTuple, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\..\..\packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll</HintPath>
</Reference>
<Reference Include="System.Web.ApplicationServices" />
<Reference Include="System.Web.DynamicData" />
<Reference Include="System.Web.Entity" />
@ -98,6 +101,7 @@
<HintPath>..\..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Razor.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
</ItemGroup>
<ItemGroup>

View File

@ -53,7 +53,7 @@ namespace Orchard.Email.Services {
smtpClient.EnableSsl = smtpSettings.EnableSsl;
smtpClient.DeliveryMethod = SmtpDeliveryMethod.Network;
context.MailMessage.From = new MailAddress(smtpSettings.Address);
context.MailMessage.From = new MailAddress(smtpSettings.FromAddress);
context.MailMessage.IsBodyHtml = !String.IsNullOrWhiteSpace(context.MailMessage.Body) && context.MailMessage.Body.Contains("<") && context.MailMessage.Body.Contains(">");
try {

View File

@ -9,7 +9,6 @@ using Orchard.ContentManagement;
using Orchard.DisplayManagement;
using Orchard.Logging;
using Orchard.Email.Models;
using System.Linq;
using System.IO;
namespace Orchard.Email.Services {
@ -41,7 +40,6 @@ namespace Orchard.Email.Services {
}
public void Process(IDictionary<string, object> parameters) {
if (!_smtpSettings.IsValid()) {
return;
}
@ -51,10 +49,15 @@ namespace Orchard.Email.Services {
Subject = Read(parameters, "Subject"),
Recipients = Read(parameters, "Recipients"),
ReplyTo = Read(parameters, "ReplyTo"),
From = Read(parameters, "From"),
FromAddress = Read(parameters, "FromAddress"),
FromName = Read(parameters, "FromName"),
Bcc = Read(parameters, "Bcc"),
Cc = Read(parameters, "CC"),
Attachments = (IEnumerable<string>)(parameters.ContainsKey("Attachments") ? parameters["Attachments"] : new List<string>())
Attachments = (IEnumerable<string>)(
parameters.ContainsKey("Attachments")
? parameters["Attachments"]
: new List<string>()
)
};
if (string.IsNullOrWhiteSpace(emailMessage.Recipients)) {
@ -62,6 +65,18 @@ namespace Orchard.Email.Services {
return;
}
var mailMessage = CreteMailMessage(parameters, emailMessage);
try {
_smtpClientField.Value.Send(mailMessage);
}
catch (Exception e) {
Logger.Error(e, "Could not send email");
}
}
private MailMessage CreteMailMessage(IDictionary<string, object> parameters, EmailMessage emailMessage) {
// Apply default Body alteration for SmtpChannel.
var template = _shapeFactory.Create("Template_Smtp_Wrapper", Arguments.From(new {
Content = new MvcHtmlString(emailMessage.Body)
@ -75,79 +90,88 @@ namespace Orchard.Email.Services {
if (parameters.ContainsKey("Message")) {
// A full message object is provided by the sender.
var oldMessage = mailMessage;
mailMessage = (MailMessage)parameters["Message"];
if (String.IsNullOrWhiteSpace(mailMessage.Subject))
if (string.IsNullOrWhiteSpace(mailMessage.Subject))
mailMessage.Subject = oldMessage.Subject;
if (String.IsNullOrWhiteSpace(mailMessage.Body)) {
if (string.IsNullOrWhiteSpace(mailMessage.Body)) {
mailMessage.Body = oldMessage.Body;
mailMessage.IsBodyHtml = oldMessage.IsBodyHtml;
}
}
try {
foreach (var recipient in ParseRecipients(emailMessage.Recipients)) {
mailMessage.To.Add(new MailAddress(recipient));
}
foreach (var recipient in ParseRecipients(emailMessage.Recipients)) {
mailMessage.To.Add(new MailAddress(recipient));
if (!string.IsNullOrWhiteSpace(emailMessage.Cc)) {
foreach (var recipient in ParseRecipients(emailMessage.Cc)) {
mailMessage.CC.Add(new MailAddress(recipient));
}
}
if (!String.IsNullOrWhiteSpace(emailMessage.Cc)) {
foreach (var recipient in ParseRecipients(emailMessage.Cc)) {
mailMessage.CC.Add(new MailAddress(recipient));
}
if (!string.IsNullOrWhiteSpace(emailMessage.Bcc)) {
foreach (var recipient in ParseRecipients(emailMessage.Bcc)) {
mailMessage.Bcc.Add(new MailAddress(recipient));
}
}
if (!String.IsNullOrWhiteSpace(emailMessage.Bcc)) {
foreach (var recipient in ParseRecipients(emailMessage.Bcc)) {
mailMessage.Bcc.Add(new MailAddress(recipient));
}
}
var senderAddress =
!string.IsNullOrWhiteSpace(emailMessage.FromAddress) ? emailMessage.FromAddress :
!string.IsNullOrWhiteSpace(_smtpSettings.FromAddress) ? _smtpSettings.FromAddress :
// Take 'From' address from site settings or web.config.
((SmtpSection)ConfigurationManager.GetSection("system.net/mailSettings/smtp")).From;
if (!String.IsNullOrWhiteSpace(emailMessage.From)) {
mailMessage.From = new MailAddress(emailMessage.From);
var senderName = !string.IsNullOrWhiteSpace(emailMessage.FromName)
? emailMessage.FromName
: _smtpSettings.FromName;
var sender = (senderAddress, senderName) switch
{
(string address, string name) => new MailAddress(address, name),
(string address, null) => new MailAddress(address),
_ => throw new InvalidOperationException("No sender email address")
};
mailMessage.From = sender;
var replyTo =
!string.IsNullOrWhiteSpace(emailMessage.ReplyTo) ? ParseRecipients(emailMessage.ReplyTo) :
!string.IsNullOrWhiteSpace(_smtpSettings.ReplyTo) ? new[] { _smtpSettings.ReplyTo } :
Array.Empty<string>();
foreach (var recipient in replyTo) {
mailMessage.ReplyToList.Add(new MailAddress(recipient));
}
foreach (var attachmentPath in emailMessage.Attachments) {
if (File.Exists(attachmentPath)) {
mailMessage.Attachments.Add(new Attachment(attachmentPath));
}
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);
throw new FileNotFoundException(T("One or more attachments not found.").Text);
}
if (!String.IsNullOrWhiteSpace(emailMessage.ReplyTo)) {
foreach (var recipient in ParseRecipients(emailMessage.ReplyTo)) {
mailMessage.ReplyToList.Add(new MailAddress(recipient));
}
}
foreach (var attachmentPath in emailMessage.Attachments) {
if (File.Exists(attachmentPath)) {
mailMessage.Attachments.Add(new Attachment(attachmentPath));
}
else {
throw new FileNotFoundException(T("One or more attachments not found.").Text);
}
}
if (parameters.ContainsKey("NotifyReadEmail")) {
if (parameters["NotifyReadEmail"] is bool) {
if ((bool)(parameters["NotifyReadEmail"])) {
mailMessage.Headers.Add("Disposition-Notification-To", mailMessage.From.ToString());
}
}
}
_smtpClientField.Value.Send(mailMessage);
}
catch (Exception e) {
Logger.Error(e, "Could not send email");
if (parameters.ContainsKey("NotifyReadEmail")) {
if (parameters["NotifyReadEmail"] is bool) {
if ((bool)(parameters["NotifyReadEmail"])) {
mailMessage.Headers.Add("Disposition-Notification-To", mailMessage.From.ToString());
}
}
}
if (!string.IsNullOrWhiteSpace(_smtpSettings.ListUnsubscribe)){
mailMessage.Headers.Add("List-Unsubscribe", _smtpSettings.ListUnsubscribe);
}
return mailMessage;
}
private SmtpClient CreateSmtpClient() {
// If no properties are set in the dashboard, use the web.config value.
if (String.IsNullOrWhiteSpace(_smtpSettings.Host)) {
if (string.IsNullOrWhiteSpace(_smtpSettings.Host)) {
return new SmtpClient();
}
@ -155,7 +179,7 @@ namespace Orchard.Email.Services {
UseDefaultCredentials = _smtpSettings.RequireCredentials && _smtpSettings.UseDefaultCredentials
};
if (!smtpClient.UseDefaultCredentials && !String.IsNullOrWhiteSpace(_smtpSettings.UserName)) {
if (!smtpClient.UseDefaultCredentials && !string.IsNullOrWhiteSpace(_smtpSettings.UserName)) {
smtpClient.Credentials = new NetworkCredential(_smtpSettings.UserName, _smtpSettings.Password);
}
@ -169,12 +193,10 @@ namespace Orchard.Email.Services {
return smtpClient;
}
private string Read(IDictionary<string, object> dictionary, string key) {
return dictionary.ContainsKey(key) ? dictionary[key] as string : null;
}
private string Read(IDictionary<string, object> dictionary, string key) =>
dictionary.ContainsKey(key) ? dictionary[key] as string : null;
private IEnumerable<string> ParseRecipients(string recipients) {
return recipients.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries);
}
private IEnumerable<string> ParseRecipients(string recipients) =>
recipients.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries);
}
}

View File

@ -3,15 +3,24 @@
@{
var smtpClient = new SmtpClient();
}
<fieldset>
<legend>@T("Email")</legend>
<div>
<label for="@Html.FieldIdFor(m => m.Address)">@T("Sender email address")</label>
@Html.TextBoxFor(m => m.Address, new { @class = "text medium", placeholder = Model.AddressPlaceholder })
@Html.ValidationMessage("Address", "*")
<label for="@Html.FieldIdFor(m => m.FromAddress)">@T("Sender email address")</label>
@Html.TextBoxFor(m => m.FromAddress, new { @class = "text medium", placeholder = Model.AddressPlaceholder })
@Html.ValidationMessage("FromAddress", "*")
<span class="hint">@T("The default email address to use as a sender.")</span>
</div>
<div>
<label for="@Html.FieldIdFor(m => m.FromName)">@T("Sender name")</label>
@Html.TextBoxFor(m => m.FromName, new { @class = "text medium" })
<span class="hint">@T("The default value to use as a sender name.")</span>
</div>
<div>
<label for="@Html.FieldIdFor(m => m.ReplyTo)">@T("Reply to address")</label>
@Html.TextBoxFor(m => m.ReplyTo, new { @class = "text medium" })
<span class="hint">@T("The default email address to use for reply to")</span>
</div>
<div>
<label for="@Html.FieldIdFor(m => m.Host)">@T("Host name")</label>
@Html.TextBoxFor(m => m.Host, new { placeholder = smtpClient.Host, @class = "text medium" })
@ -35,9 +44,7 @@
<label for="@Html.FieldIdFor(m => m.RequireCredentials)" class="forcheckbox">@T("Require credentials")</label>
@Html.ValidationMessage("RequireCredentials", "*")
</div>
<div data-controllerid="@Html.FieldIdFor(m => m.RequireCredentials)">
<div>
@Html.RadioButtonFor(m => m.UseDefaultCredentials, false, new { id = "customCredentialsOption", name = "UseDefaultCredentials" })
<label for="customCredentialsOption" class="forcheckbox">@T("Specify username/password")</label>
@ -65,6 +72,11 @@
</span>
</div>
</div>
<div>
<label for="@Html.FieldIdFor(m => m.ListUnsubscribe)">@T("List-Unsubscribe header")</label>
@Html.TextBoxFor(m => m.ListUnsubscribe, new { @class = "text medium" })
<span class="hint">@T("A mailto:link to unsubscribe a user when clicking unsubscribe option")</span>
</div>
</fieldset>
<fieldset>
<legend>@T("Test those settings:")</legend>
@ -84,7 +96,9 @@
var url = "@Url.Action("TestSettings", "EmailAdmin", new {area = "Orchard.Email"})",
error = $("#emailtesterror"),
info = $("#emailtestinfo"),
from = $("#@Html.FieldIdFor(m => m.Address)"),
fromAddress = $("#@Html.FieldIdFor(m => m.FromAddress)"),
fromName = $("#@Html.FieldIdFor(m => m.FromName)"),
replyTo = $("#@Html.FieldIdFor(m => m.ReplyTo)"),
host = $("#@Html.FieldIdFor(m => m.Host)"),
port = $("#@Html.FieldIdFor(m => m.Port)"),
enableSsl = $("#@Html.FieldIdFor(m => m.EnableSsl)"),
@ -92,11 +106,14 @@
useDefaultCredentials = $("input[name='@Html.NameFor(m => m.UseDefaultCredentials)']"),
userName = $("#@Html.FieldIdFor(m => m.UserName)"),
password = $("#@Html.FieldIdFor(m => m.Password)"),
listUnsubscribe = $("#@Html.FieldIdFor(m => m.ListUnsubscribe)"),
to = $("#emailtestto");
$("#emailtestsend").click(function () {
$.post(url, {
from: from.val(),
fromAddress: fromAddress.val(),
fromName: fromName.val(),
replyTo: replyTo.val(),
host: host.val(),
port: port.val(),
enableSsl: enableSsl.prop("checked"),
@ -104,6 +121,7 @@
useDefaultCredentials: useDefaultCredentials.filter(':checked').val(),
userName: userName.val(),
password: password.val(),
listUnsubscribe: listUnsubscribe.val(),
to: to.val(),
__RequestVerificationToken: to.closest("form").find("input[name=__RequestVerificationToken]").val()
})

View File

@ -6,4 +6,5 @@
<package id="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" version="2.0.1" targetFramework="net452" />
<package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net452" />
<package id="Newtonsoft.Json" version="12.0.2" targetFramework="net452" />
<package id="System.ValueTuple" version="4.5.0" targetFramework="net461" />
</packages>