From 19da9c3cd8248e2d89ee6a938be9dfe55d601ca1 Mon Sep 17 00:00:00 2001 From: MattMofDoom <30714560+MattMofDoom@users.noreply.github.com> Date: Thu, 12 Aug 2021 12:04:43 +1000 Subject: [PATCH 01/36] Implement mail host fallback and DNS delivery, fix To address parsing, set SmtpOptions to use Lazy --- src/Seq.App.EmailPlus/DeliveryType.cs | 12 ++ src/Seq.App.EmailPlus/DirectMailGateway.cs | 124 ++++++++++++++++-- src/Seq.App.EmailPlus/DnsMailResult.cs | 14 ++ src/Seq.App.EmailPlus/EmailApp.cs | 81 ++++++++++-- src/Seq.App.EmailPlus/HandlebarsHelpers.cs | 1 - src/Seq.App.EmailPlus/IMailGateway.cs | 2 +- src/Seq.App.EmailPlus/MailResult.cs | 6 +- .../Seq.App.EmailPlus.csproj | 1 + src/Seq.App.EmailPlus/SmtpOptions.cs | 20 ++- .../EmailReactorTests.cs | 17 ++- .../Support/CollectingMailGateway.cs | 10 +- .../Support/SentMessage.cs | 4 +- 12 files changed, 254 insertions(+), 38 deletions(-) create mode 100644 src/Seq.App.EmailPlus/DeliveryType.cs create mode 100644 src/Seq.App.EmailPlus/DnsMailResult.cs 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 262b8bd..3df7c30 100644 --- a/src/Seq.App.EmailPlus/DirectMailGateway.cs +++ b/src/Seq.App.EmailPlus/DirectMailGateway.cs @@ -1,5 +1,9 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using DnsClient; +using DnsClient.Protocol; using MailKit.Net.Smtp; using MimeKit; @@ -7,25 +11,125 @@ namespace Seq.App.EmailPlus { class DirectMailGateway : IMailGateway { + readonly SmtpClient _client = new SmtpClient(); + public async Task Send(SmtpOptions options, MimeMessage message) { if (message == null) throw new ArgumentNullException(nameof(message)); + var mailResult = new MailResult(); + var type = DeliveryType.MailHost; + foreach (var server in options.ServerList) + { + mailResult = await TryDeliver(server, options, message, type); + if (!mailResult.Success) + type = DeliveryType.MailFallback; + else + break; + } + + return mailResult; + } + + public async Task SendDns(DeliveryType deliveryType, SmtpOptions options, MimeMessage message) + { + var dnsResult = new DnsMailResult {Success = true}; + var resultList = new List(); + var lastServer = string.Empty; + if (message == null) throw new ArgumentNullException(nameof(message)); + var type = deliveryType; + + try + { + var domains = GetDomains(message); + + var dnsClient = new LookupClient(); + 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; + dnsResult.LastServer = server; + dnsResult.Type = type; + mailResult = await TryDeliver(server, options, message, type); + resultList.Add(mailResult); + + if (mailResult.Success) + break; + type = DeliveryType.DnsFallback; + } + + if (mailResult.Success) continue; + dnsResult.LastServer = lastServer; + dnsResult.Success = false; + break; + } + } + catch (Exception ex) + { + dnsResult.Type = type; + dnsResult.LastServer = lastServer; + dnsResult.Success = false; + dnsResult.LastError = ex; + } + + dnsResult.Results = resultList; + return dnsResult; + } + + + private 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; + foreach (var domain in domains.Where(domain => + !toDomain.Equals(domain, StringComparison.OrdinalIgnoreCase))) + { + domains.Add(toDomain); + } + } + + return domains; + } + + private 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 { - var client = new SmtpClient(); - - await client.ConnectAsync(options.Server, options.Port, options.SocketOptions); - if (options.RequiresAuthentication) - await client.AuthenticateAsync(options.User, options.Password); - await client.SendAsync(message); - await client.DisconnectAsync(true); - - return new MailResult {Success = true}; + if (!string.IsNullOrEmpty(server)) + { + await _client.ConnectAsync(server, options.Port, options.SocketOptions); + if (options.RequiresAuthentication) + await _client.AuthenticateAsync(options.User, options.Password); + } + else + { + var dnsClient = new LookupClient(); + } + + 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, Errors = ex}; + return new MailResult {Success = false, Errors = 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..7aede1b --- /dev/null +++ b/src/Seq.App.EmailPlus/DnsMailResult.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; + +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 Results { get; set; } + } +} diff --git a/src/Seq.App.EmailPlus/EmailApp.cs b/src/Seq.App.EmailPlus/EmailApp.cs index 94e5f92..27302f3 100644 --- a/src/Seq.App.EmailPlus/EmailApp.cs +++ b/src/Seq.App.EmailPlus/EmailApp.cs @@ -3,10 +3,8 @@ using System.Collections.Generic; using System.Dynamic; using System.Linq; -using System.Net; using System.Threading.Tasks; using HandlebarsDotNet; -using MailKit.Net.Smtp; using MailKit.Security; using MimeKit; using Seq.Apps; @@ -25,7 +23,7 @@ public class EmailApp : SeqApp, ISubscribeToAsync readonly IMailGateway _mailGateway = new DirectMailGateway(); readonly ConcurrentDictionary _lastSeen = new ConcurrentDictionary(); readonly Lazy