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

Envelope enhancements #83

Open
wants to merge 51 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 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
e695402
Add email priority and priority property mapping
MattMofDoom Aug 13, 2021
abf8d25
Add cc, bcc, and plain text body
MattMofDoom Aug 13, 2021
2a35adc
Add replyTo, improved email logging
MattMofDoom Aug 13, 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
259d7b9
Merge branch 'DeliveryEnhancements' into EnvelopeEnhancements
MattMofDoom Aug 22, 2021
6733a87
Correct merge error and handle null replyTo/cc/bcc, resolve several R…
MattMofDoom Aug 22, 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
23bdff3
Merge branch 'DeliveryEnhancements' into EnvelopeEnhancements
MattMofDoom Aug 27, 2021
3a8531d
Reinstate and extend CorrectSecureSocketOptionsAreChosenForPort from #86
MattMofDoom Aug 28, 2021
349ef77
Merge branch 'DeliveryEnhancements' into EnvelopeEnhancements
MattMofDoom Aug 28, 2021
38d0c5a
Improve error/delivery logging for consistency
MattMofDoom Aug 28, 2021
16fca06
Merge branch 'DeliveryEnhancements' into EnvelopeEnhancements
MattMofDoom Aug 28, 2021
3e2e207
Update SeqApp description
MattMofDoom Aug 28, 2021
d300106
Add lastserrver to unhandled mail error
MattMofDoom Aug 31, 2021
ad780f4
Merge branch 'DeliveryEnhancements' into EnvelopeEnhancements
MattMofDoom Aug 31, 2021
78e2044
Make sure we attempt all domains
MattMofDoom Aug 31, 2021
1ad5b7a
Merge branch 'DeliveryEnhancements' into EnvelopeEnhancements
MattMofDoom Aug 31, 2021
41886be
Catch all recipient domains in the envelope for processing
MattMofDoom Aug 31, 2021
98e6047
Always catch LastError in the Errors list, always set Success
MattMofDoom Aug 31, 2021
2d8fa19
Merge branch 'DeliveryEnhancements' into EnvelopeEnhancements
MattMofDoom Aug 31, 2021
2700698
Use an enum and implement migration from EnableSsl per #89
MattMofDoom Oct 16, 2021
1aebdbc
Merge branch 'DeliveryEnhancements' into EnvelopeEnhancements
MattMofDoom Oct 16, 2021
d8d7f06
Merge #81 changes resulting from #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
947de5b
Merge branch 'DeliveryEnhancements' into EnvelopeEnhancements
MattMofDoom Nov 1, 2021
500439c
Merge changes resulting from #93, and merge to DeliveryEnhancements b…
MattMofDoom Nov 1, 2021
0e26adb
Update dependencies
MattMofDoom Jan 31, 2022
4b2e26c
Merge remote-tracking branch 'origin/DeliveryEnhancements' into Envel…
MattMofDoom Jan 31, 2022
0b81ada
Merge branch 'dev' into EnvelopeEnhancements
MattMofDoom May 19, 2022
37a89b8
Minimal reversion of changes preventing Visual Studio earlier than 2022
MattMofDoom May 19, 2022
b2d43e4
Update dependencies
MattMofDoom May 19, 2022
1160326
Revert to earlier build script to address error
MattMofDoom May 19, 2022
012cc97
Move build back to VS 2019 to address error
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
}
}
196 changes: 188 additions & 8 deletions src/Seq.App.EmailPlus/DirectMailGateway.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,203 @@
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);
}

foreach (var cc in message.Cc)
{
var ccDomain = cc.ToString().Split('@')[1];
if (string.IsNullOrEmpty(ccDomain)) continue;
if (!domains.Any(domain => domain.Equals(ccDomain, StringComparison.OrdinalIgnoreCase)))
domains.Add(ccDomain);
}


foreach (var bcc in message.Bcc)
{
var bccDomain = bcc.ToString().Split('@')[1];
if (string.IsNullOrEmpty(bccDomain)) continue;
if (!domains.Any(domain => domain.Equals(bccDomain, StringComparison.OrdinalIgnoreCase)))
domains.Add(bccDomain);
}

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