diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/157_email_multiple_addressesFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/157_email_multiple_addressesFixture.cs new file mode 100644 index 000000000..c2b91cca3 --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Migration/157_email_multiple_addressesFixture.cs @@ -0,0 +1,100 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Migration +{ + [TestFixture] + public class email_multiple_addressesFixture : MigrationTest + { + [Test] + public void should_convert_to_list_on_email_lists() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Notifications").Row(new + { + OnGrab = true, + OnDownload = true, + OnUpgrade = true, + OnHealthIssue = true, + IncludeHealthWarnings = true, + OnRename = true, + Name = "Mail Sonarr", + Implementation = "Email", + Tags = "[]", + Settings = new EmailSettings173 + { + Server = "smtp.gmail.com", + Port = 563, + To = "dont@email.me" + }.ToJson(), + ConfigContract = "EmailSettings" + }); + }); + + var items = db.Query("SELECT * FROM Notifications"); + + items.Should().HaveCount(1); + items.First().Implementation.Should().Be("Email"); + items.First().ConfigContract.Should().Be("EmailSettings"); + items.First().Settings.To.Count().Should().Be(1); + } + } + + public class NotificationDefinition173 + { + public int Id { get; set; } + public string Implementation { get; set; } + public string ConfigContract { get; set; } + public EmailSettings174 Settings { get; set; } + public string Name { get; set; } + public bool OnGrab { get; set; } + public bool OnDownload { get; set; } + public bool OnUpgrade { get; set; } + public bool OnRename { get; set; } + public bool OnSeriesDelete { get; set; } + public bool OnEpisodeFileDelete { get; set; } + public bool OnEpisodeFileDeleteForUpgrade { get; set; } + public bool OnHealthIssue { get; set; } + public bool SupportsOnGrab { get; set; } + public bool SupportsOnDownload { get; set; } + public bool SupportsOnUpgrade { get; set; } + public bool SupportsOnRename { get; set; } + public bool SupportsOnSeriesDelete { get; set; } + public bool SupportsOnEpisodeFileDelete { get; set; } + public bool SupportsOnEpisodeFileDeleteForUpgrade { get; set; } + public bool SupportsOnHealthIssue { get; set; } + public bool IncludeHealthWarnings { get; set; } + public List Tags { get; set; } + } + + public class EmailSettings173 + { + public string Server { get; set; } + public int Port { get; set; } + public bool RequireEncryption { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string From { get; set; } + public string To { get; set; } + } + + public class EmailSettings174 + { + public string Server { get; set; } + public int Port { get; set; } + public bool RequireEncryption { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string From { get; set; } + public IEnumerable To { get; set; } + public IEnumerable Cc { get; set; } + public IEnumerable Bcc { get; set; } + } +} diff --git a/src/NzbDrone.Core.Test/NotificationTests/EmailTests/EmailSettingsValidatorFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/EmailTests/EmailSettingsValidatorFixture.cs new file mode 100644 index 000000000..90340579b --- /dev/null +++ b/src/NzbDrone.Core.Test/NotificationTests/EmailTests/EmailSettingsValidatorFixture.cs @@ -0,0 +1,113 @@ +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Notifications.Email; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.NotificationTests.EmailTests +{ + [TestFixture] + public class EmailSettingsValidatorFixture : CoreTest + { + private EmailSettings _emailSettings; + private TestValidator _validator; + + [SetUp] + public void Setup() + { + _validator = new TestValidator + { + v => v.RuleFor(s => s).SetValidator(Subject) + }; + + _emailSettings = Builder.CreateNew() + .With(s => s.Server = "someserver") + .With(s => s.Port = 567) + .With(s => s.RequireEncryption = true) + .With(s => s.From = "dont@email.me") + .With(s => s.To = new string[] { "dont@email.me" }) + .Build(); + } + + [Test] + public void should_be_valid_if_all_settings_valid() + { + _validator.Validate(_emailSettings).IsValid.Should().BeTrue(); + } + + [Test] + public void should_not_be_valid_if_port_is_out_of_range() + { + _emailSettings.Port = 900000; + + _validator.Validate(_emailSettings).IsValid.Should().BeFalse(); + } + + [Test] + public void should_not_be_valid_if_server_is_empty() + { + _emailSettings.Server = ""; + + _validator.Validate(_emailSettings).IsValid.Should().BeFalse(); + } + + [Test] + public void should_not_be_valid_if_from_is_empty() + { + _emailSettings.From = ""; + + _validator.Validate(_emailSettings).IsValid.Should().BeFalse(); + } + + [TestCase("sonarr")] + [TestCase("sonarr@sonarr")] + [TestCase("email.me")] + public void should_not_be_valid_if_from_is_invalid(string email) + { + _emailSettings.From = email; + + _validator.Validate(_emailSettings).IsValid.Should().BeFalse(); + } + + [TestCase("sonarr")] + [TestCase("sonarr@sonarr")] + [TestCase("email.me")] + public void should_not_be_valid_if_to_is_invalid(string email) + { + _emailSettings.To = new string[] { email }; + + _validator.Validate(_emailSettings).IsValid.Should().BeFalse(); + } + + [TestCase("sonarr")] + [TestCase("sonarr@sonarr")] + [TestCase("email.me")] + public void should_not_be_valid_if_cc_is_invalid(string email) + { + _emailSettings.Cc = new string[] { email }; + + _validator.Validate(_emailSettings).IsValid.Should().BeFalse(); + } + + [TestCase("sonarr")] + [TestCase("sonarr@sonarr")] + [TestCase("email.me")] + public void should_not_be_valid_if_bcc_is_invalid(string email) + { + _emailSettings.Bcc = new string[] { email }; + + _validator.Validate(_emailSettings).IsValid.Should().BeFalse(); + } + + [Test] + public void should_not_be_valid_if_to_bcc_cc_are_all_empty() + { + _emailSettings.To = new string[] { }; + _emailSettings.Cc = new string[] { }; + _emailSettings.Bcc = new string[] { }; + + _validator.Validate(_emailSettings).IsValid.Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/157_email_multiple_addresses.cs b/src/NzbDrone.Core/Datastore/Migration/157_email_multiple_addresses.cs new file mode 100644 index 000000000..a3070699e --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/157_email_multiple_addresses.cs @@ -0,0 +1,50 @@ +using System.Data; +using System.Linq; +using FluentMigrator; +using Newtonsoft.Json.Linq; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(157)] + public class email_multiple_addresses : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(ChangeEmailAddressType); + } + + private void ChangeEmailAddressType(IDbConnection conn, IDbTransaction tran) + { + + using (var getEmailCmd = conn.CreateCommand()) + { + getEmailCmd.Transaction = tran; + getEmailCmd.CommandText = "SELECT Id, Settings FROM Notifications WHERE Implementation = 'Email'"; + + using (var reader = getEmailCmd.ExecuteReader()) + { + while (reader.Read()) + { + var id = reader.GetInt32(0); + var settings = Json.Deserialize(reader.GetString(1)); + + // "To" was changed from string to array + settings["to"] = new JArray(settings["to"].ToObject().Split(',').Select(v => v.Trim()).ToArray()); + + using (var updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "UPDATE Notifications SET Settings = ? WHERE Id = ?"; + updateCmd.AddParameter(settings.ToJson()); + updateCmd.AddParameter(id); + + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Email/EmailService.cs b/src/NzbDrone.Core/Notifications/Email/EmailService.cs index 3374f8133..7d1082ff5 100644 --- a/src/NzbDrone.Core/Notifications/Email/EmailService.cs +++ b/src/NzbDrone.Core/Notifications/Email/EmailService.cs @@ -26,25 +26,11 @@ namespace NzbDrone.Core.Notifications.Email public void SendEmail(EmailSettings settings, string subject, string body, bool htmlBody = false) { var email = new MimeMessage(); - - try - { - email.From.Add(MailboxAddress.Parse(settings.From)); - } - catch (Exception ex) - { - _logger.Error(ex, "From email address '{0}' invalid", settings.From); - } - - try - { - email.To.Add(MailboxAddress.Parse(settings.To)); - } - catch (Exception ex) - { - _logger.Error(ex, "To email address '{0}' invalid", settings.To); - } - + email.From.Add(ParseAddress("From", settings.From)); + email.To.AddRange(settings.To.Select(x => ParseAddress("To", x))); + email.Cc.AddRange(settings.Cc.Select(x => ParseAddress("CC", x))); + email.Bcc.AddRange(settings.Bcc.Select(x => ParseAddress("BCC", x))); + email.Subject = subject; email.Body = new TextPart(htmlBody ? "html" : "plain") { @@ -113,5 +99,18 @@ namespace NzbDrone.Core.Notifications.Email return null; } + + private MailboxAddress ParseAddress(string type, string address) + { + try + { + return MailboxAddress.Parse(address); + } + catch (Exception ex) + { + _logger.Error(ex, "{0} email address '{1}' invalid", type, address); + throw; + } + } } } diff --git a/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs b/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs index 0ee52dafe..c76ac94f9 100644 --- a/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs +++ b/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs @@ -1,4 +1,7 @@ -using FluentValidation; +using System; +using System.Collections.Generic; +using System.Linq; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -12,7 +15,14 @@ namespace NzbDrone.Core.Notifications.Email RuleFor(c => c.Server).NotEmpty(); RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.From).NotEmpty(); - RuleFor(c => c.To).NotEmpty(); + RuleForEach(c => c.To).EmailAddress(); + RuleForEach(c => c.Cc).EmailAddress(); + RuleForEach(c => c.Bcc).EmailAddress(); + + // Only require one of three send fields to be set + RuleFor(c => c.To).NotEmpty().Unless(c => c.Bcc.Any() || c.Cc.Any()); + RuleFor(c => c.Cc).NotEmpty().Unless(c => c.To.Any() || c.Bcc.Any()); + RuleFor(c => c.Bcc).NotEmpty().Unless(c => c.To.Any() || c.Cc.Any()); } } @@ -23,6 +33,10 @@ namespace NzbDrone.Core.Notifications.Email public EmailSettings() { Port = 587; + + To = Array.Empty(); + Cc = Array.Empty(); + Bcc = Array.Empty(); } [FieldDefinition(0, Label = "Server", HelpText = "Hostname or IP of Email server")] @@ -43,8 +57,14 @@ namespace NzbDrone.Core.Notifications.Email [FieldDefinition(5, Label = "From Address")] public string From { get; set; } - [FieldDefinition(6, Label = "Recipient Address")] - public string To { get; set; } + [FieldDefinition(6, Label = "Recipient Address(es)", HelpText = "Comma seperated list of email recipients")] + public IEnumerable To { get; set; } + + [FieldDefinition(7, Label = "CC Address(es)", HelpText = "Comma seperated list of email cc recipients", Advanced = true)] + public IEnumerable Cc { get; set; } + + [FieldDefinition(8, Label = "BCC Address(es)", HelpText = "Comma seperated list of email bcc recipients", Advanced = true)] + public IEnumerable Bcc { get; set; } public NzbDroneValidationResult Validate() { diff --git a/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs b/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs index f7ccdd5aa..d9301fa1d 100644 --- a/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs +++ b/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs @@ -246,7 +246,7 @@ namespace Sonarr.Http.ClientSchema } else { - return fieldValue.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + return fieldValue.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => v.Trim()); } }; }