diff --git a/src/Umbraco.Core/Events/SendEmailEventArgs.cs b/src/Umbraco.Core/Events/SendEmailEventArgs.cs index 720f125d9c..c1e626c6c1 100644 --- a/src/Umbraco.Core/Events/SendEmailEventArgs.cs +++ b/src/Umbraco.Core/Events/SendEmailEventArgs.cs @@ -1,5 +1,5 @@ using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Email; namespace Umbraco.Cms.Core.Events { diff --git a/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs b/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs index 51e31b1811..6e679ddbb1 100644 --- a/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs +++ b/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Mail; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Email; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; diff --git a/src/Umbraco.Core/Mail/IEmailSender.cs b/src/Umbraco.Core/Mail/IEmailSender.cs index 5b5adf71eb..45959a5a9a 100644 --- a/src/Umbraco.Core/Mail/IEmailSender.cs +++ b/src/Umbraco.Core/Mail/IEmailSender.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Email; namespace Umbraco.Cms.Core.Mail { diff --git a/src/Umbraco.Core/Mail/NotImplementedEmailSender.cs b/src/Umbraco.Core/Mail/NotImplementedEmailSender.cs index 3632e79c23..29265b4038 100644 --- a/src/Umbraco.Core/Mail/NotImplementedEmailSender.cs +++ b/src/Umbraco.Core/Mail/NotImplementedEmailSender.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Email; namespace Umbraco.Cms.Core.Mail { diff --git a/src/Umbraco.Core/Models/EmailMessage.cs b/src/Umbraco.Core/Models/Email/EmailMessage.cs similarity index 96% rename from src/Umbraco.Core/Models/EmailMessage.cs rename to src/Umbraco.Core/Models/Email/EmailMessage.cs index 153e8f33f7..55e30f2150 100644 --- a/src/Umbraco.Core/Models/EmailMessage.cs +++ b/src/Umbraco.Core/Models/Email/EmailMessage.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models.Email { public class EmailMessage { @@ -33,7 +33,6 @@ namespace Umbraco.Cms.Core.Models public EmailMessage(string from, string[] to, string[] cc, string[] bcc, string[] replyTo, string subject, string body, bool isBodyHtml, IEnumerable attachments) { - ArgumentIsNotNullOrEmpty(from, nameof(from)); ArgumentIsNotNullOrEmpty(to, nameof(to)); ArgumentIsNotNullOrEmpty(subject, nameof(subject)); ArgumentIsNotNullOrEmpty(body, nameof(body)); diff --git a/src/Umbraco.Core/Models/EmailMessageAttachment.cs b/src/Umbraco.Core/Models/Email/EmailMessageAttachment.cs similarity index 88% rename from src/Umbraco.Core/Models/EmailMessageAttachment.cs rename to src/Umbraco.Core/Models/Email/EmailMessageAttachment.cs index ee4f3ef8cb..bbb24b69f7 100644 --- a/src/Umbraco.Core/Models/EmailMessageAttachment.cs +++ b/src/Umbraco.Core/Models/Email/EmailMessageAttachment.cs @@ -1,6 +1,6 @@ using System.IO; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models.Email { public class EmailMessageAttachment { diff --git a/src/Umbraco.Core/Models/Email/NotificationEmailAddress.cs b/src/Umbraco.Core/Models/Email/NotificationEmailAddress.cs new file mode 100644 index 0000000000..755947c6a4 --- /dev/null +++ b/src/Umbraco.Core/Models/Email/NotificationEmailAddress.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Cms.Core.Models.Email +{ + /// + /// Represents an email address used for notifications. Contains both the address and its display name. + /// + public class NotificationEmailAddress + { + public string DisplayName { get; } + + public string Address { get; } + + public NotificationEmailAddress(string address, string displayName) + { + Address = address; + DisplayName = displayName; + } + } +} diff --git a/src/Umbraco.Core/Models/Email/NotificationEmailModel.cs b/src/Umbraco.Core/Models/Email/NotificationEmailModel.cs new file mode 100644 index 0000000000..a606e8e680 --- /dev/null +++ b/src/Umbraco.Core/Models/Email/NotificationEmailModel.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Cms.Core.Models.Email +{ + /// + /// Represents an email when sent with notifications. + /// + public class NotificationEmailModel + { + public NotificationEmailAddress From { get; } + + public IEnumerable To { get; } + + public IEnumerable Cc { get; } + + public IEnumerable Bcc { get; } + + public IEnumerable ReplyTo { get; } + + public string Subject { get; } + + public string Body { get; } + + public bool IsBodyHtml { get; } + + public IList Attachments { get; } + + public bool HasAttachments => Attachments != null && Attachments.Count > 0; + + public NotificationEmailModel( + NotificationEmailAddress from, + IEnumerable to, + IEnumerable cc, + IEnumerable bcc, + IEnumerable replyTo, + string subject, + string body, + IEnumerable attachments, + bool isBodyHtml) + { + From = from; + To = to; + Cc = cc; + Bcc = bcc; + ReplyTo = replyTo; + Subject = subject; + Body = body; + IsBodyHtml = isBodyHtml; + Attachments = attachments?.ToList(); + } + + } +} diff --git a/src/Umbraco.Core/Notifications/SendEmailNotification.cs b/src/Umbraco.Core/Notifications/SendEmailNotification.cs index 194ee68edc..3c9caabb0e 100644 --- a/src/Umbraco.Core/Notifications/SendEmailNotification.cs +++ b/src/Umbraco.Core/Notifications/SendEmailNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Email; namespace Umbraco.Cms.Core.Notifications { public class SendEmailNotification : INotification { - public SendEmailNotification(EmailMessage message) => Message = message; + public SendEmailNotification(NotificationEmailModel message) => Message = message; - public EmailMessage Message { get; set; } + public NotificationEmailModel Message { get; } } } diff --git a/src/Umbraco.Infrastructure/EmailSender.cs b/src/Umbraco.Infrastructure/EmailSender.cs index 02e83405d6..72daad7de1 100644 --- a/src/Umbraco.Infrastructure/EmailSender.cs +++ b/src/Umbraco.Infrastructure/EmailSender.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Mail; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Email; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Infrastructure.Extensions; using SmtpClient = MailKit.Net.Smtp.SmtpClient; @@ -53,7 +53,8 @@ namespace Umbraco.Cms.Infrastructure { if (enableNotification) { - await _eventAggregator.PublishAsync(new SendEmailNotification(message)); + await _eventAggregator.PublishAsync( + new SendEmailNotification(message.ToNotificationEmail(_globalSettings.Smtp?.From))); } return; } diff --git a/src/Umbraco.Infrastructure/Extensions/EmailMessageExtensions.cs b/src/Umbraco.Infrastructure/Extensions/EmailMessageExtensions.cs index cc1efa271e..f7bed9d74c 100644 --- a/src/Umbraco.Infrastructure/Extensions/EmailMessageExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/EmailMessageExtensions.cs @@ -1,7 +1,8 @@ using System; +using System.Collections.Generic; using MimeKit; using MimeKit.Text; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Email; namespace Umbraco.Cms.Infrastructure.Extensions { @@ -9,13 +10,9 @@ namespace Umbraco.Cms.Infrastructure.Extensions { public static MimeMessage ToMimeMessage(this EmailMessage mailMessage, string configuredFromAddress) { - var fromEmail = mailMessage.From; - if (string.IsNullOrEmpty(fromEmail)) - { - fromEmail = configuredFromAddress; - } + var fromEmail = string.IsNullOrEmpty(mailMessage.From) ? configuredFromAddress : mailMessage.From; - if (!InternetAddress.TryParse(mailMessage.From, out InternetAddress fromAddress)) + if (!InternetAddress.TryParse(fromEmail, out InternetAddress fromAddress)) { throw new ArgumentException($"Email could not be sent. Could not parse from address {fromEmail} as a valid email address."); } @@ -78,5 +75,57 @@ namespace Umbraco.Cms.Infrastructure.Extensions throw new InvalidOperationException($"Email could not be sent. Could not parse a valid recipient address."); } } + + public static NotificationEmailModel ToNotificationEmail(this EmailMessage emailMessage, + string configuredFromAddress) + { + var fromEmail = string.IsNullOrEmpty(emailMessage.From) ? configuredFromAddress : emailMessage.From; + + NotificationEmailAddress from = ToNotificationAddress(fromEmail); + + return new NotificationEmailModel(from, + GetNotificationAddresses(emailMessage.To), + GetNotificationAddresses(emailMessage.Cc), + GetNotificationAddresses(emailMessage.Bcc), + GetNotificationAddresses(emailMessage.ReplyTo), + emailMessage.Subject, + emailMessage.Body, + emailMessage.Attachments, + emailMessage.IsBodyHtml); + } + + private static NotificationEmailAddress ToNotificationAddress(string address) + { + if (InternetAddress.TryParse(address, out InternetAddress internetAddress)) + { + if (internetAddress is MailboxAddress mailboxAddress) + { + return new NotificationEmailAddress(mailboxAddress.Address, internetAddress.Name); + } + } + + return null; + } + + private static IEnumerable GetNotificationAddresses(IEnumerable addresses) + { + if (addresses is null) + { + return null; + } + + var notificationAddresses = new List(); + + foreach (var address in addresses) + { + NotificationEmailAddress notificationAddress = ToNotificationAddress(address); + if (notificationAddress is not null) + { + notificationAddresses.Add(notificationAddress); + } + } + + return notificationAddresses; + } } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/NotificationService.cs b/src/Umbraco.Infrastructure/Services/Implement/NotificationService.cs index aedad0e56b..c6f440942e 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/NotificationService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/NotificationService.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Mail; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Email; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Repositories; diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Extensions/EmailMessageExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Extensions/EmailMessageExtensionsTests.cs index 7915b6b92c..709873e8a3 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Extensions/EmailMessageExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Extensions/EmailMessageExtensionsTests.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text; using NUnit.Framework; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Email; using Umbraco.Cms.Infrastructure.Extensions; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Extensions @@ -24,9 +25,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Extensions const string subject = "Subject"; const string body = "

Message

"; const bool isBodyHtml = true; - var emailMesasge = new EmailMessage(from, to, subject, body, isBodyHtml); + var emailMessage = new EmailMessage(from, to, subject, body, isBodyHtml); - var result = emailMesasge.ToMimeMessage(ConfiguredSender); + var result = emailMessage.ToMimeMessage(ConfiguredSender); Assert.AreEqual(1, result.From.Count()); Assert.AreEqual(from, result.From.First().ToString()); @@ -54,9 +55,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Extensions { new EmailMessageAttachment(attachmentStream, "test.txt"), }; - var emailMesasge = new EmailMessage(from, to, cc, bcc, replyTo, subject, body, isBodyHtml, attachments); + var emailMessage = new EmailMessage(from, to, cc, bcc, replyTo, subject, body, isBodyHtml, attachments); - var result = emailMesasge.ToMimeMessage(ConfiguredSender); + var result = emailMessage.ToMimeMessage(ConfiguredSender); Assert.AreEqual(1, result.From.Count()); Assert.AreEqual(from, result.From.First().ToString()); @@ -77,5 +78,147 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Extensions Assert.AreEqual(body, result.TextBody.ToString()); Assert.AreEqual(1, result.Attachments.Count()); } + + [Test] + public void Can_Construct_MimeMessage_With_ConfiguredSender() + { + const string to = "to@email.com"; + const string subject = "Subject"; + const string body = "

Message

"; + const bool isBodyHtml = true; + var emailMessage = new EmailMessage(null, to, subject, body, isBodyHtml); + + var result = emailMessage.ToMimeMessage(ConfiguredSender); + + Assert.AreEqual(1, result.From.Count()); + Assert.AreEqual(ConfiguredSender, result.From.First().ToString()); + Assert.AreEqual(1, result.To.Count()); + Assert.AreEqual(to, result.To.First().ToString()); + Assert.AreEqual(subject, result.Subject); + Assert.IsNull(result.TextBody); + Assert.AreEqual(body, result.HtmlBody.ToString()); + } + + [Test] + public void Can_Construct_NotificationEmailModel_From_Simple_MailMessage() + { + const string from = "from@email.com"; + const string to = "to@email.com"; + const string subject = "Subject"; + const string body = "

Message

"; + const bool isBodyHtml = true; + var emailMessage = new EmailMessage(from, to, subject, body, isBodyHtml); + + NotificationEmailModel result = emailMessage.ToNotificationEmail(ConfiguredSender); + + Assert.AreEqual(from, result.From.Address); + Assert.AreEqual("", result.From.DisplayName); + Assert.AreEqual(1, result.To.Count()); + Assert.AreEqual(to, result.To.First().Address); + Assert.AreEqual("", result.To.First().DisplayName); + Assert.AreEqual(subject, result.Subject); + Assert.AreEqual(body, result.Body); + Assert.IsTrue(result.IsBodyHtml); + Assert.IsFalse(result.HasAttachments); + } + + [Test] + public void Can_Construct_NotificationEmailModel_From_Simple_MailMessage_With_Configured_Sender() + { + const string to = "to@email.com"; + const string subject = "Subject"; + const string body = "

Message

"; + const bool isBodyHtml = true; + var emailMessage = new EmailMessage(null, to, subject, body, isBodyHtml); + + NotificationEmailModel result = emailMessage.ToNotificationEmail(ConfiguredSender); + + Assert.AreEqual(ConfiguredSender, result.From.Address); + Assert.AreEqual("", result.From.DisplayName); + Assert.AreEqual(1, result.To.Count()); + Assert.AreEqual(to, result.To.First().Address); + Assert.AreEqual("", result.To.First().DisplayName); + Assert.AreEqual(subject, result.Subject); + Assert.AreEqual(body, result.Body); + Assert.IsTrue(result.IsBodyHtml); + Assert.IsFalse(result.HasAttachments); + } + + [Test] + public void Can_Construct_NotificationEmailModel_From_Simple_MailMessage_With_DisplayName() + { + const string from = "\"From Email\" "; + const string to = "\"To Email\" "; + const string subject = "Subject"; + const string body = "

Message

"; + const bool isBodyHtml = true; + var emailMessage = new EmailMessage(from, to, subject, body, isBodyHtml); + + NotificationEmailModel result = emailMessage.ToNotificationEmail(ConfiguredSender); + + Assert.AreEqual("from@from.com", result.From.Address); + Assert.AreEqual("From Email", result.From.DisplayName); + Assert.AreEqual(1, result.To.Count()); + Assert.AreEqual("to@to.com", result.To.First().Address); + Assert.AreEqual("To Email", result.To.First().DisplayName); + Assert.AreEqual(subject, result.Subject); + Assert.AreEqual(body, result.Body); + Assert.IsTrue(result.IsBodyHtml); + Assert.IsFalse(result.HasAttachments); + } + + + [Test] + public void Can_Construct_NotificationEmailModel_From_Full_EmailMessage() + { + const string from = "\"From Email\" "; + string[] to = { "to@email.com", "\"Second Email\" ", "invalid@invalid@invalid" }; + string[] cc = { "\"First CC\" ", "cc2@email.com", "invalid@invalid@invalid" }; + string[] bcc = { "bcc@email.com", "bcc2@email.com", "\"Third BCC\" ", "invalid@email@address" }; + string[] replyTo = { "replyto@email.com", "invalid@invalid@invalid" }; + const string subject = "Subject"; + const string body = "Message"; + const bool isBodyHtml = false; + + using var attachmentStream = new MemoryStream(Encoding.UTF8.GetBytes("test")); + var attachments = new List + { + new EmailMessageAttachment(attachmentStream, "test.txt"), + }; + var emailMessage = new EmailMessage(from, to, cc, bcc, replyTo, subject, body, isBodyHtml, attachments); + + var result = emailMessage.ToNotificationEmail(ConfiguredSender); + + Assert.AreEqual("from@from.com", result.From.Address); + Assert.AreEqual("From Email", result.From.DisplayName); + + Assert.AreEqual(2, result.To.Count()); + Assert.AreEqual("to@email.com", result.To.First().Address); + Assert.AreEqual("", result.To.First().DisplayName); + Assert.AreEqual("to2@email.com", result.To.Skip(1).First().Address); + Assert.AreEqual("Second Email", result.To.Skip(1).First().DisplayName); + + Assert.AreEqual(2, result.Cc.Count()); + Assert.AreEqual("cc@email.com", result.Cc.First().Address); + Assert.AreEqual("First CC", result.Cc.First().DisplayName); + Assert.AreEqual("cc2@email.com", result.Cc.Skip(1).First().Address); + Assert.AreEqual("", result.Cc.Skip(1).First().DisplayName); + + Assert.AreEqual(3, result.Bcc.Count()); + Assert.AreEqual("bcc@email.com", result.Bcc.First().Address); + Assert.AreEqual("", result.Bcc.First().DisplayName); + Assert.AreEqual("bcc2@email.com", result.Bcc.Skip(1).First().Address); + Assert.AreEqual("", result.Bcc.Skip(1).First().DisplayName); + Assert.AreEqual("bcc3@email.com", result.Bcc.Skip(2).First().Address); + Assert.AreEqual("Third BCC", result.Bcc.Skip(2).First().DisplayName); + + Assert.AreEqual(1, result.ReplyTo.Count()); + Assert.AreEqual("replyto@email.com", result.ReplyTo.First().Address); + Assert.AreEqual("", result.ReplyTo.First().DisplayName); + + Assert.AreEqual(subject, result.Subject); + Assert.AreEqual(body, result.Body); + Assert.AreEqual(1, result.Attachments.Count()); + } } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index 707c486f3e..c552c0d976 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -17,6 +17,7 @@ using Umbraco.Cms.Core.Mail; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Email; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Security; diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index 19def88456..ee09b7d67b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -26,6 +26,7 @@ using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Email; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services;