diff --git a/Build.ps1 b/Build.ps1 index 5f4a1e6..8bf0f0d 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -10,7 +10,7 @@ if(Test-Path .\artifacts) { } & dotnet restore --no-cache -if($LASTEXITCODE -ne 0) { throw "Build failed with exit code $LASTEXITCODE" } +if($LASTEXITCODE -ne 0) { exit 1 } $branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL]; $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; @@ -30,7 +30,7 @@ foreach ($src in ls src/*) { & dotnet publish -c Release -o ./obj/publish & dotnet pack -c Release -o ..\..\artifacts --no-build } - if($LASTEXITCODE -ne 0) { throw "Build failed with exit code $LASTEXITCODE" } + if($LASTEXITCODE -ne 0) { exit 1 } Pop-Location } @@ -41,9 +41,9 @@ foreach ($test in ls test/*.Tests) { echo "build: Testing project in $test" & dotnet test -c Release - if($LASTEXITCODE -ne 0) { throw "Build failed with exit code $LASTEXITCODE" } + if($LASTEXITCODE -ne 0) { exit 3 } Pop-Location } -Pop-Location +Pop-Location \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 137223f..98c8907 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,6 @@ version: '{build}' skip_tags: true -image: Visual Studio 2022 +image: Visual Studio 2019 install: build_script: - pwsh: ./Build.ps1 diff --git a/global.json b/bak/global.json similarity index 100% rename from global.json rename to bak/global.json diff --git a/src/Seq.App.EmailPlus/DeliveryType.cs b/src/Seq.App.EmailPlus/DeliveryType.cs new file mode 100644 index 0000000..ae985a5 --- /dev/null +++ b/src/Seq.App.EmailPlus/DeliveryType.cs @@ -0,0 +1,12 @@ +namespace Seq.App.EmailPlus +{ + public enum DeliveryType + { + MailHost, + MailFallback, + Dns, + DnsFallback, + HostDnsFallback, + None = -1 + } +} diff --git a/src/Seq.App.EmailPlus/DirectMailGateway.cs b/src/Seq.App.EmailPlus/DirectMailGateway.cs index 17a2c7f..7fd6dec 100644 --- a/src/Seq.App.EmailPlus/DirectMailGateway.cs +++ b/src/Seq.App.EmailPlus/DirectMailGateway.cs @@ -1,23 +1,186 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using DnsClient; +using DnsClient.Protocol; using MailKit.Net.Smtp; +using MailKit.Security; using MimeKit; namespace Seq.App.EmailPlus { class DirectMailGateway : IMailGateway { - public async Task SendAsync(SmtpOptions options, MimeMessage message) + static readonly SmtpClient Client = new SmtpClient(); + static readonly LookupClient DnsClient = new LookupClient(); + + public async Task SendAsync(SmtpOptions options, MimeMessage message) { if (message == null) throw new ArgumentNullException(nameof(message)); + var mailResult = new MailResult(); + var type = DeliveryType.MailHost; + var errors = new List(); + foreach (var server in options.Host) + { + mailResult = await TryDeliver(server, options, message, type); + if (!mailResult.Success) + { + errors.Add(mailResult.LastError); + type = DeliveryType.MailFallback; + } + else + { + break; + } + } + + mailResult.Errors = errors; + return mailResult; + } + + public async Task SendDnsAsync(DeliveryType deliveryType, SmtpOptions options, + MimeMessage message) + { + var dnsResult = new DnsMailResult(); + var resultList = new List(); + var lastServer = string.Empty; + var errors = new List(); + if (message == null) throw new ArgumentNullException(nameof(message)); + var type = deliveryType; + + try + { + var domains = GetDomains(message).ToList(); + var successCount = 0; + + foreach (var domain in domains) + { + type = deliveryType; + lastServer = domain; + var mx = await DnsClient.QueryAsync(domain, QueryType.MX); + var mxServers = + (from mxServer in mx.Answers + where !string.IsNullOrEmpty(((MxRecord)mxServer).Exchange) + select ((MxRecord)mxServer).Exchange).Select(dummy => (string)dummy).ToList(); + var mailResult = new MailResult(); + foreach (var server in mxServers) + { + lastServer = server; + mailResult = await TryDeliver(server, options, message, type); + var lastError = dnsResult.LastError; + errors.AddRange(mailResult.Errors); + + dnsResult = new DnsMailResult + { + LastServer = server, + LastError = mailResult.LastError ?? lastError, + Type = type, + }; + + resultList.Add(mailResult); + + if (mailResult.Success) + break; + type = DeliveryType.DnsFallback; + } + + if (mailResult.Success) + { + successCount++; + continue; + } + + if (dnsResult.LastError != null) continue; + dnsResult.LastError = mxServers.Count == 0 + ? new Exception("DNS delivery failed - no MX records detected for " + domain) + : new Exception("DNS delivery failed - no error detected"); + dnsResult.Success = false; + dnsResult.Errors.Add(dnsResult.LastError); + } + + if (!domains.Any()) + { + dnsResult.Success = false; + dnsResult.LastError = + new Exception("DNS delivery failed - no domains parsed from recipient addresses"); + } + + if (successCount < domains.Count) + { + if (successCount == 0) + { + dnsResult.Success = false; + dnsResult.LastError = + new Exception("DNS delivery failure - no domains could be successfully delivered."); + } + else + { + dnsResult.Success = true; // A qualified success ... + dnsResult.LastError = + new Exception( + $"DNS delivery partial failure - {domains.Count - successCount} of {successCount} domains could not be delivered."); + } + + dnsResult.Errors.Add(dnsResult.LastError); + } + else + { + dnsResult.Success = true; + } + } + catch (Exception ex) + { + dnsResult = new DnsMailResult + { + Type = type, + LastServer = lastServer, + Success = false, + LastError = ex + }; + } + + dnsResult.Errors = errors; + dnsResult.Results = resultList; + return dnsResult; + } + + + public static IEnumerable GetDomains(MimeMessage message) + { + var domains = new List(); + foreach (var to in message.To) + { + var toDomain = to.ToString().Split('@')[1]; + if (string.IsNullOrEmpty(toDomain)) continue; + if (!domains.Any(domain => domain.Equals(toDomain, StringComparison.OrdinalIgnoreCase))) + domains.Add(toDomain); + } + + return domains; + } + + private static async Task TryDeliver(string server, SmtpOptions options, MimeMessage message, + DeliveryType deliveryType) + { + if (string.IsNullOrEmpty(server)) + return new MailResult {Success = false, LastServer = server, Type = deliveryType}; + try + { + await Client.ConnectAsync(server, options.Port, (SecureSocketOptions)options.SocketOptions); + if (options.RequiresAuthentication) + await Client.AuthenticateAsync(options.Username, options.Password); + + + await Client.SendAsync(message); + await Client.DisconnectAsync(true); - var client = new SmtpClient(); - - await client.ConnectAsync(options.Host, options.Port, options.SocketOptions); - if (options.RequiresAuthentication) - await client.AuthenticateAsync(options.Username, options.Password); - await client.SendAsync(message); - await client.DisconnectAsync(true); + return new MailResult {Success = true, LastServer = server, Type = deliveryType}; + } + catch (Exception ex) + { + return new MailResult {Success = false, LastError = ex, LastServer = server, Type = deliveryType}; + } } } } \ No newline at end of file diff --git a/src/Seq.App.EmailPlus/DnsMailResult.cs b/src/Seq.App.EmailPlus/DnsMailResult.cs new file mode 100644 index 0000000..3fc296d --- /dev/null +++ b/src/Seq.App.EmailPlus/DnsMailResult.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +// ReSharper disable UnusedAutoPropertyAccessor.Global + +namespace Seq.App.EmailPlus +{ + public class DnsMailResult + { + public bool Success { get; set; } + public DeliveryType Type { get; set; } + public string LastServer { get; set; } + public Exception LastError { get; set; } + public List Errors { get; set; } = new List(); + public List Results { get; set; } = new List(); + } +} diff --git a/src/Seq.App.EmailPlus/EmailApp.cs b/src/Seq.App.EmailPlus/EmailApp.cs index a1982c2..b589751 100644 --- a/src/Seq.App.EmailPlus/EmailApp.cs +++ b/src/Seq.App.EmailPlus/EmailApp.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Threading.Tasks; using HandlebarsDotNet; -using MailKit.Security; using MimeKit; using Seq.Apps; using Seq.Apps.LogEvents; @@ -53,30 +52,45 @@ public EmailApp() [SeqAppSetting( DisplayName = "To address", - HelpText = "The account to which the email is being sent. Multiple addresses are separated by a comma. Handlebars syntax is supported.")] + HelpText = + "The account to which the email is being sent. Multiple addresses are separated by a comma. Handlebars syntax is supported.")] public string To { get; set; } [SeqAppSetting( IsOptional = true, DisplayName = "Subject template", - HelpText = "The subject of the email, using Handlebars syntax. If blank, a default subject will be generated.")] + HelpText = + "The subject of the email, using Handlebars syntax. If blank, a default subject will be generated.")] public string SubjectTemplate { get; set; } [SeqAppSetting( - HelpText = "The name of the SMTP server machine.")] + DisplayName = "SMTP Mail Host(s)", + HelpText = + "The name of the SMTP server machine. Optionally specify fallback hosts as comma-delimited string. If not specified, Deliver Using DNS should be enabled.", + IsOptional = true)] public new string Host { get; set; } + [SeqAppSetting( + DisplayName = "Deliver using DNS", + HelpText = + "Deliver directly using DNS. If SMTP Mail Host(s) is configured, this will be used as a fallback delivery mechanism. If not enabled, SMTP Mail Host(s) should be configured.")] + public bool? DeliverUsingDns { get; set; } + [SeqAppSetting( IsOptional = true, - HelpText = "The port on the SMTP server machine to send mail to. Leave this blank to use the standard port (25).")] + HelpText = + "The port on the SMTP server machine to send mail to. Leave this blank to use the standard port (25).")] public int? Port { get; set; } + //EnableSSL has been retired but is preserved for migration purposes + public bool? EnableSsl { get; set; } + [SeqAppSetting( IsOptional = true, DisplayName = "Require TLS", HelpText = "Check this box to require that the server supports SSL/TLS for sending messages. If the port used is 465, " + "implicit SSL will be enabled; otherwise, the STARTTLS extension will be used.")] - public bool? EnableSsl { get; set; } + public TlsOptions? EnableTls { get; set; } [SeqAppSetting( IsOptional = true, @@ -106,15 +120,17 @@ public EmailApp() protected override void OnAttached() { + if (string.IsNullOrEmpty(Host) && (DeliverUsingDns == null || !(bool) DeliverUsingDns)) + throw new Exception("There are no delivery methods selected - you must specify at least one SMTP Mail Host, or enable Deliver Using DNS"); + var port = Port ?? DefaultPort; _options = _options = new SmtpOptions( Host, + DeliverUsingDns != null && (bool)DeliverUsingDns, port, - EnableSsl ?? false - ? RequireSslForPort(port) - : SecureSocketOptions.StartTlsWhenAvailable, + SmtpOptions.GetSocketOptions(port, EnableSsl, EnableTls), Username, - Password); + Password);; _subjectTemplate = Handlebars.Compile(string.IsNullOrEmpty(SubjectTemplate) ? DefaultSubjectTemplate @@ -125,16 +141,18 @@ protected override void OnAttached() _toAddressesTemplate = string.IsNullOrEmpty(To) ? (_, __) => To : Handlebars.Compile(To); } + public async Task OnAsync(Event evt) { if (ShouldSuppress(evt)) return; var to = FormatTemplate(_toAddressesTemplate, evt, base.Host) - .Split(new[]{','}, StringSplitOptions.RemoveEmptyEntries); + .Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries) + .Select(t => t.Trim()).ToList(); - if (to.Length == 0) + if (to.Count == 0) { - Log.Warning("Email 'to' address template did not evaluate to one or more recipient addresses"); + Log.ForContext("To", _toAddressesTemplate).Error("Email 'to' address template did not evaluate to one or more recipient addresses - email cannot be sent!"); return; } @@ -144,13 +162,73 @@ public async Task OnAsync(Event evt) if (subject.Length > MaxSubjectLength) subject = subject.Substring(0, MaxSubjectLength); - await _mailGateway.SendAsync( - _options, - new MimeMessage( - new[] {MailboxAddress.Parse(From)}, - to.Select(MailboxAddress.Parse), - subject, - new BodyBuilder {HtmlBody = body}.ToMessageBody())); + var toList = to.Select(MailboxAddress.Parse).ToList(); + var sent = false; + var logged = false; + var type = DeliveryType.None; + var message = new MimeMessage( + new List {InternetAddress.Parse(From)}, + toList, subject, new BodyBuilder {HtmlBody = body}.ToMessageBody()); + + var errors = new List(); + var lastServer = string.Empty; + if (_options.Host != null && _options.Host.Any()) + { + type = DeliveryType.MailHost; + var result = await _mailGateway.SendAsync(_options, message); + errors = result.Errors; + sent = result.Success; + lastServer = result.LastServer; + + if (!result.Success) + { + Log.ForContext("From", From).ForContext("To", to).ForContext("Subject", subject) + .ForContext("Success", sent).ForContext("Body", body) + .ForContext(nameof(result.LastServer), result.LastServer) + .ForContext(nameof(result.Type), result.Type).ForContext(nameof(result.Errors), result.Errors) + .Error(result.LastError, + "Error sending mail: {Message}, From: {From}, To: {To}, Subject: {Subject}", + result.LastError?.Message, From, to, subject); + logged = true; + } + } + + if (!sent && _options.DnsDelivery) + { + type = type == DeliveryType.None ? DeliveryType.Dns : DeliveryType.HostDnsFallback; + var result = await _mailGateway.SendDnsAsync(type, _options, message); + errors = result.Errors; + sent = result.Success; + lastServer = result.LastServer; + type = result.Type; + + if (!result.Success) + { + Log.ForContext("From", From).ForContext("To", to).ForContext("Subject", subject) + .ForContext("Success", sent).ForContext("Body", body) + .ForContext(nameof(result.Results), result.Results, true).ForContext("Errors", errors) + .ForContext(nameof(result.Type), result.Type).ForContext(nameof(result.LastServer), result.LastServer) + .Error(result.LastError, + "Error sending mail via DNS: {Message}, From: {From}, To: {To}, Subject: {Subject}", + result.LastError?.Message, From, to, subject); + logged = true; + } + } + + if (sent) + { + Log.ForContext("From", From).ForContext("To", to).ForContext("Subject", subject) + .ForContext("Success", true).ForContext("Body", body) + .ForContext("LastServer", lastServer).ForContext("Errors", errors) + .ForContext("Type", type) + .Information("Mail Sent, From: {From}, To: {To}, Subject: {Subject}", From, to, subject); + } + else if (!logged) + Log.ForContext("From", From).ForContext("To", to).ForContext("Subject", subject) + .ForContext("Success", true).ForContext("Body", body).ForContext("Errors", errors) + .ForContext("Type", type).ForContext("LastServer", lastServer) + .Error("Unhandled mail error, From: {From}, To: {To}, Subject: {Subject}", From, to, subject); + } bool ShouldSuppress(Event evt) @@ -182,30 +260,29 @@ bool ShouldSuppress(Event evt) return true; } - internal static SecureSocketOptions RequireSslForPort(int port) - { - return (port == DefaultSslPort ? SecureSocketOptions.SslOnConnect : SecureSocketOptions.StartTls); - } - internal static string FormatTemplate(Template template, Event evt, Host host) { - var properties = (IDictionary) ToDynamic(evt.Data.Properties ?? new Dictionary()); + var properties = + (IDictionary) ToDynamic(evt.Data.Properties ?? new Dictionary()); - var payload = (IDictionary) ToDynamic(new Dictionary + var payload = (IDictionary) ToDynamic(new Dictionary { - { "$Id", evt.Id }, - { "$UtcTimestamp", evt.TimestampUtc }, - { "$LocalTimestamp", evt.Data.LocalTimestamp }, - { "$Level", evt.Data.Level }, - { "$MessageTemplate", evt.Data.MessageTemplate }, - { "$Message", evt.Data.RenderedMessage }, - { "$Exception", evt.Data.Exception }, - { "$Properties", properties }, - { "$EventType", "$" + evt.EventType.ToString("X8") }, - { "$Instance", host.InstanceName }, - { "$ServerUri", host.BaseUri }, + {"$Id", evt.Id}, + {"$UtcTimestamp", evt.TimestampUtc}, + {"$LocalTimestamp", evt.Data.LocalTimestamp}, + {"$Level", evt.Data.Level}, + {"$MessageTemplate", evt.Data.MessageTemplate}, + {"$Message", evt.Data.RenderedMessage}, + {"$Exception", evt.Data.Exception}, + {"$Properties", properties}, + {"$EventType", "$" + evt.EventType.ToString("X8")}, + {"$Instance", host.InstanceName}, + {"$ServerUri", host.BaseUri}, // Note, this will only be valid when events are streamed directly to the app, and not when the app is sending an alert notification. - { "$EventUri", string.Concat(host.BaseUri, "#/events?filter=@Id%20%3D%20'", evt.Id, "'&show=expanded") } + { + "$EventUri", + string.Concat(host.BaseUri, "#/events?filter=@Id%20%3D%20'", evt.Id, "'&show=expanded") + } }); foreach (var property in properties) @@ -234,5 +311,10 @@ static object ToDynamic(object o) return o; } + + public SmtpOptions GetOptions() + { + return _options; + } } } diff --git a/src/Seq.App.EmailPlus/HandlebarsHelpers.cs b/src/Seq.App.EmailPlus/HandlebarsHelpers.cs index fae0363..8a8cb3d 100644 --- a/src/Seq.App.EmailPlus/HandlebarsHelpers.cs +++ b/src/Seq.App.EmailPlus/HandlebarsHelpers.cs @@ -2,7 +2,6 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; -using System.IO; using System.Linq; namespace Seq.App.EmailPlus diff --git a/src/Seq.App.EmailPlus/IMailGateway.cs b/src/Seq.App.EmailPlus/IMailGateway.cs index ed599a2..6baa09a 100644 --- a/src/Seq.App.EmailPlus/IMailGateway.cs +++ b/src/Seq.App.EmailPlus/IMailGateway.cs @@ -5,6 +5,7 @@ namespace Seq.App.EmailPlus { interface IMailGateway { - Task SendAsync(SmtpOptions options, MimeMessage message); + Task SendAsync(SmtpOptions options, MimeMessage message); + Task SendDnsAsync(DeliveryType deliveryType, SmtpOptions options, MimeMessage message); } } \ No newline at end of file diff --git a/src/Seq.App.EmailPlus/MailResult.cs b/src/Seq.App.EmailPlus/MailResult.cs index 6aa6fa1..0bbf3af 100644 --- a/src/Seq.App.EmailPlus/MailResult.cs +++ b/src/Seq.App.EmailPlus/MailResult.cs @@ -1,10 +1,14 @@ using System; +using System.Collections.Generic; namespace Seq.App.EmailPlus { public class MailResult { - public bool Success; - public Exception Errors; + public bool Success { get; set; } + public DeliveryType Type { get; set; } + public string LastServer { get; set; } + public Exception LastError { get; set; } + public List Errors { get; set; } = new List(); } } \ No newline at end of file diff --git a/src/Seq.App.EmailPlus/Seq.App.EmailPlus.csproj b/src/Seq.App.EmailPlus/Seq.App.EmailPlus.csproj index 5082600..99f6a41 100644 --- a/src/Seq.App.EmailPlus/Seq.App.EmailPlus.csproj +++ b/src/Seq.App.EmailPlus/Seq.App.EmailPlus.csproj @@ -14,20 +14,24 @@ True LICENSE + Seq.App.EmailPlus + - - - + + + + - + + diff --git a/src/Seq.App.EmailPlus/SmtpOptions.cs b/src/Seq.App.EmailPlus/SmtpOptions.cs index ec2d0c5..720abc8 100644 --- a/src/Seq.App.EmailPlus/SmtpOptions.cs +++ b/src/Seq.App.EmailPlus/SmtpOptions.cs @@ -1,25 +1,56 @@ using System; +using System.Collections.Generic; +using System.Linq; using MailKit.Security; namespace Seq.App.EmailPlus { - class SmtpOptions + public class SmtpOptions { - public string Host { get; } - public int Port { get; } - public string Username { get; } - public string Password { get; } - public SecureSocketOptions SocketOptions { get; } - + public List Host { get; set; } = new List(); + public bool DnsDelivery { get; set; } + public int Port { get; set; } = 25; + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; public bool RequiresAuthentication => !string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(Password); + public TlsOptions SocketOptions { get; set; } - public SmtpOptions(string host, int port, SecureSocketOptions socketOptions, string username = null, string password = null) + public SmtpOptions(string host, bool dnsDelivery, int port, TlsOptions socketOptions, string username = null, string password = null) { - Host = host ?? throw new ArgumentNullException(nameof(host)); + Host = GetServerList(host).ToList(); + DnsDelivery = dnsDelivery; Port = port; Username = username; Password = password; SocketOptions = socketOptions; } + + IEnumerable GetServerList(string hostName) + { + if (!string.IsNullOrEmpty(hostName)) + return hostName.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries) + .Select(t => t.Trim()).ToList(); + return new List(); + } + + public static TlsOptions GetSocketOptions(int port, bool? enableSsl, TlsOptions? enableTls) + { + if (enableSsl == null && enableTls == null) return TlsOptions.Auto; + + switch (enableTls) + { + case null when (bool)enableSsl && port == 465: //Implicit TLS + case TlsOptions.None when port == 465: + case TlsOptions.Auto when port == 465: + case TlsOptions.StartTlsWhenAvailable when port == 465: + return TlsOptions.SslOnConnect; + case null when (bool)enableSsl: + return TlsOptions.StartTls; //Explicit TLS + case null: + return TlsOptions.Auto; + default: + return (TlsOptions)enableTls; + } + } } } diff --git a/src/Seq.App.EmailPlus/TlsOptions.cs b/src/Seq.App.EmailPlus/TlsOptions.cs new file mode 100644 index 0000000..dc865e9 --- /dev/null +++ b/src/Seq.App.EmailPlus/TlsOptions.cs @@ -0,0 +1,29 @@ +using MailKit.Security; + +namespace Seq.App.EmailPlus +{ + public enum TlsOptions + { + /// + /// None + /// + None = SecureSocketOptions.None, + /// + /// Auto + /// + Auto = SecureSocketOptions.Auto, + /// + /// Implicit TLS + /// + SslOnConnect = SecureSocketOptions.SslOnConnect, + /// + /// Explicit TLS + /// + StartTls = SecureSocketOptions.StartTls, + /// + /// Optional TLS + /// + StartTlsWhenAvailable = SecureSocketOptions.StartTlsWhenAvailable + + } +} diff --git a/test/Seq.App.EmailPlus.Tests/EmailAppTests.cs b/test/Seq.App.EmailPlus.Tests/EmailAppTests.cs index 6513ae9..07d9b4f 100644 --- a/test/Seq.App.EmailPlus.Tests/EmailAppTests.cs +++ b/test/Seq.App.EmailPlus.Tests/EmailAppTests.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using MailKit.Security; +using MimeKit; using Seq.App.EmailPlus.Tests.Support; using Seq.Apps; using Seq.Apps.LogEvents; @@ -131,6 +133,32 @@ public async Task ToAddressesAreTemplated() Assert.Equal("test@example.com", to.ToString()); } + [Fact] + public void FallbackHostsCalculated() + { + var mail = new CollectingMailGateway(); + var reactor = new EmailApp(mail, new SystemClock()) + { + From = "from@example.com", + To = "{{Name}}@example.com", + Host = "example.com,example2.com" + }; + + reactor.Attach(new TestAppHost()); + Assert.True(reactor.GetOptions().Host.Count() == 2); + } + + [Fact] + public void ParseDomainTest() + { + var mail = new DirectMailGateway(); + var domains = DirectMailGateway.GetDomains(new MimeMessage( + new List {InternetAddress.Parse("test@example.com")}, + new List {InternetAddress.Parse("test2@example.com"), InternetAddress.Parse("test3@example.com"), InternetAddress.Parse("test@example2.com")}, "Test", + (new BodyBuilder {HtmlBody = "test"}).ToMessageBody())); + Assert.True(domains.Count() == 2); + } + [Fact] public async Task EventsAreSuppressedWithinWindow() { @@ -203,12 +231,19 @@ public async Task ToAddressesCanBeCommaSeparated() } [Theory] - [InlineData(25, SecureSocketOptions.StartTls)] - [InlineData(587, SecureSocketOptions.StartTls)] - [InlineData(465, SecureSocketOptions.SslOnConnect)] - public void CorrectSecureSocketOptionsAreChosenForPort(int port, SecureSocketOptions expected) + [InlineData(25, null, null, TlsOptions.Auto)] + [InlineData(25, true, null, TlsOptions.StartTls)] + [InlineData(25, false, TlsOptions.None, TlsOptions.None)] + [InlineData(25, false, TlsOptions.StartTlsWhenAvailable, TlsOptions.StartTlsWhenAvailable)] + [InlineData(587, true, TlsOptions.StartTls, TlsOptions.StartTls)] + [InlineData(587, false, TlsOptions.None, TlsOptions.None)] + [InlineData(587, false, TlsOptions.StartTlsWhenAvailable, TlsOptions.StartTlsWhenAvailable)] + [InlineData(465, true, TlsOptions.None, TlsOptions.SslOnConnect)] + [InlineData(465, false, TlsOptions.Auto, TlsOptions.SslOnConnect)] + [InlineData(465, false, TlsOptions.SslOnConnect, TlsOptions.SslOnConnect)] + public void CorrectSecureSocketOptionsAreChosenForPort(int port, bool? enableSsl, TlsOptions? enableTls, TlsOptions expected) { - Assert.Equal(expected, EmailApp.RequireSslForPort(port)); + Assert.Equal(expected, SmtpOptions.GetSocketOptions(port, enableSsl, enableTls)); } } } diff --git a/test/Seq.App.EmailPlus.Tests/Seq.App.EmailPlus.Tests.csproj b/test/Seq.App.EmailPlus.Tests/Seq.App.EmailPlus.Tests.csproj index e277edf..592898e 100644 --- a/test/Seq.App.EmailPlus.Tests/Seq.App.EmailPlus.Tests.csproj +++ b/test/Seq.App.EmailPlus.Tests/Seq.App.EmailPlus.Tests.csproj @@ -1,14 +1,15 @@  - net6.0 + netstandard2.0 false + f8a5b3bb-84c9-42d5-b8f2-bf20abad0ec8 - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Seq.App.EmailPlus.Tests/Support/CollectingMailGateway.cs b/test/Seq.App.EmailPlus.Tests/Support/CollectingMailGateway.cs index d5e2ac7..088e021 100644 --- a/test/Seq.App.EmailPlus.Tests/Support/CollectingMailGateway.cs +++ b/test/Seq.App.EmailPlus.Tests/Support/CollectingMailGateway.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Threading.Tasks; -using MailKit.Net.Smtp; using MimeKit; namespace Seq.App.EmailPlus.Tests.Support @@ -9,10 +8,18 @@ class CollectingMailGateway : IMailGateway { public List Sent { get; } = new List(); - public Task SendAsync(SmtpOptions options, MimeMessage message) + public async Task SendAsync(SmtpOptions options, MimeMessage message) { - Sent.Add(new SentMessage(message)); - return Task.CompletedTask; + await Task.Run(() => Sent.Add(new SentMessage(message))); + + return new MailResult {Success = true}; + } + + public async Task SendDnsAsync(DeliveryType deliveryType, SmtpOptions options, MimeMessage message) + { + await Task.Run(() => Sent.Add(new SentMessage(message))); + + return new DnsMailResult {Success = true}; } } } diff --git a/test/Seq.App.EmailPlus.Tests/Support/SentMessage.cs b/test/Seq.App.EmailPlus.Tests/Support/SentMessage.cs index ebd7712..4e1a6ee 100644 --- a/test/Seq.App.EmailPlus.Tests/Support/SentMessage.cs +++ b/test/Seq.App.EmailPlus.Tests/Support/SentMessage.cs @@ -1,6 +1,4 @@ - -using MailKit.Net.Smtp; -using MimeKit; +using MimeKit; namespace Seq.App.EmailPlus.Tests.Support {