Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delivery enhancements #81

Open
wants to merge 36 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
19da9c3
Implement mail host fallback and DNS delivery, fix To address parsing…
MattMofDoom Aug 12, 2021
189f988
Remove extant code from TryDeliver
MattMofDoom Aug 12, 2021
1a45cc3
Improve DnsMailResult logic
MattMofDoom Aug 12, 2021
bffb82a
Retain lastError if not superseded
MattMofDoom Aug 12, 2021
26261af
Always track list of exceptions for logging
MattMofDoom Aug 12, 2021
6c3f9f3
Corrections from running mail host and DNS test cases
MattMofDoom Aug 12, 2021
4d499d1
Merge remote-tracking branch 'upstream/dev' into DeliveryEnhancements
MattMofDoom Aug 17, 2021
4b726fa
Merge with upstream conflicts
MattMofDoom Aug 17, 2021
7b74c41
Handle self comments made on #81
MattMofDoom Aug 17, 2021
c2ad37e
lastError local var no longer needed, update tests to reflect SecureS…
MattMofDoom Aug 17, 2021
67db746
Minor code issues from Resharper
MattMofDoom Aug 17, 2021
42864c6
Only need to parse list of servers once, handle no delivery methods b…
MattMofDoom Aug 17, 2021
c0f9953
Update help text for clarity
MattMofDoom Aug 17, 2021
5396dae
Merge branch 'dev' into DeliveryEnhancements
MattMofDoom Aug 18, 2021
fbf8a4f
Adjust code for upstream changes
MattMofDoom Aug 18, 2021
e7bc50a
GetServerList doesn't need to be public
MattMofDoom Aug 18, 2021
c6ef0ab
Merge remote-tracking branch 'upstream/dev' into DeliveryEnhancements
MattMofDoom Aug 27, 2021
72cec0b
Harmonise #86 with delivery enhancements
MattMofDoom Aug 27, 2021
b85428b
Correct port
MattMofDoom Aug 27, 2021
3a8531d
Reinstate and extend CorrectSecureSocketOptionsAreChosenForPort from #86
MattMofDoom Aug 28, 2021
38d0c5a
Improve error/delivery logging for consistency
MattMofDoom Aug 28, 2021
d300106
Add lastserrver to unhandled mail error
MattMofDoom Aug 31, 2021
78e2044
Make sure we attempt all domains
MattMofDoom Aug 31, 2021
98e6047
Always catch LastError in the Errors list, always set Success
MattMofDoom Aug 31, 2021
2700698
Use an enum and implement migration from EnableSsl per #89
MattMofDoom Oct 16, 2021
c8a5b38
Merge remote-tracking branch 'upstream/dev' into DeliveryEnhancements
MattMofDoom Nov 1, 2021
a1a600f
Merge changes from #93
MattMofDoom Nov 1, 2021
0e26adb
Update dependencies
MattMofDoom Jan 31, 2022
6be299b
Merge branch 'dev' into DeliveryEnhancements
MattMofDoom May 19, 2022
651cf1f
Minimal reversion of changes preventing Visual Studio earlier than 2022
MattMofDoom May 19, 2022
82ce698
Update dependencies
MattMofDoom May 19, 2022
ea72087
Test to resolve build problem
MattMofDoom May 19, 2022
d456005
Test 2
MattMofDoom May 19, 2022
a28aa10
Trying downgrade AppVeyor build to 2019
MattMofDoom May 19, 2022
77875ae
Remove check for build failure due to downgrade
MattMofDoom May 19, 2022
e6acf92
Revert to earlier build script
MattMofDoom May 19, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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
}
Expand All @@ -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
2 changes: 1 addition & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
version: '{build}'
skip_tags: true
image: Visual Studio 2022
image: Visual Studio 2019
install:
build_script:
- pwsh: ./Build.ps1
Expand Down
File renamed without changes.
12 changes: 12 additions & 0 deletions src/Seq.App.EmailPlus/DeliveryType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Seq.App.EmailPlus
{
public enum DeliveryType
{
MailHost,
MailFallback,
Dns,
DnsFallback,
HostDnsFallback,
None = -1
}
}
179 changes: 171 additions & 8 deletions src/Seq.App.EmailPlus/DirectMailGateway.cs
Original file line number Diff line number Diff line change
@@ -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<MailResult> 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<Exception>();
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<DnsMailResult> SendDnsAsync(DeliveryType deliveryType, SmtpOptions options,
MimeMessage message)
{
var dnsResult = new DnsMailResult();
var resultList = new List<MailResult>();
var lastServer = string.Empty;
var errors = new List<Exception>();
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<string> GetDomains(MimeMessage message)
{
var domains = new List<string>();
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<MailResult> 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};
}
}
}
}
16 changes: 16 additions & 0 deletions src/Seq.App.EmailPlus/DnsMailResult.cs
Original file line number Diff line number Diff line change
@@ -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<Exception> Errors { get; set; } = new List<Exception>();
public List<MailResult> Results { get; set; } = new List<MailResult>();
}
}
Loading