Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 10 additions & 3 deletions server/Mailist.Tests/MimeMessageCreationServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ public async Task TestNameLookup()

var emailRelay = ActivatorUtilities.CreateInstance<MimeMessageCreationService>(serviceProvider);

byte[] header;
using (MemoryStream headerStream = new())
{
var headerList = new HeaderList();
await headerList.WriteToAsync(headerStream, TestContext.Current.CancellationToken);
header = headerStream.ToArray();
}

byte[] body;
using (MemoryStream bodyStream = new())
{
Expand All @@ -48,11 +56,10 @@ public async Task TestNameLookup()
replyTo: null,
to: "test@example.org",
receiver: "test@example.org",
header: null,
header: header,
body: body);

var recipient = MailboxAddress.Parse("max.mustermann@example.com");
var mimeMessage = await emailRelay.PrepareForForwardTo(inboxEmail, recipient, TestContext.Current.CancellationToken);
var mimeMessage = await emailRelay.PrepareForward(inboxEmail, TestContext.Current.CancellationToken);
if (mimeMessage.From[0] is MailboxAddress from)
{
Assert.Equal("Armin Adendorf", from.Name);
Expand Down
5 changes: 4 additions & 1 deletion server/Mailist/EmailDelivery/EmailDeliveryJobController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,15 @@ protected override async ValueTask ExecuteJob(OutboxEmail outboxEmail, Cancellat
{
SmtpClient smtp = await GetConnection(cancellationToken);

MailboxAddress sender = new(name: null, options.Value.SenderAddress);
MailboxAddress recipient = new(name: null, outboxEmail.EmailAddress);

using MemoryStream memoryStream = new(outboxEmail.Content);
using MimeMessage mimeMessage = MimeMessage.Load(memoryStream, CancellationToken.None);

try
{
await smtp.SendAsync(mimeMessage, cancellationToken);
await smtp.SendAsync(mimeMessage, sender, [recipient], cancellationToken);

database.OutboxEmails.Remove(outboxEmail);
database.SentEmails.Add(new SentEmail
Expand Down
10 changes: 7 additions & 3 deletions server/Mailist/EmailRelay/EmailRelayJobController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,13 @@ protected override async ValueTask ExecuteJob(InboxEmail email, CancellationToke
MailboxAddress[] recipients = await distributionListService.GetRecipients(distributionList, cancellationToken);
foreach (MailboxAddress address in recipients)
{
using MimeMessage preparedMessage = distributionList.Flags.HasFlag(DistributionListFlags.OverrideRecipient)
? await mimeMessageService.PrepareForForwardTo(email, address, cancellationToken)
: mimeMessageService.PrepareForResentTo(email, address);
using MimeMessage preparedMessage = await mimeMessageService.PrepareForward(email, cancellationToken);
if (distributionList.Flags.HasFlag(DistributionListFlags.OverrideRecipient))
{
preparedMessage.To.Clear();
preparedMessage.Cc.Clear();
preparedMessage.To.Add(address);
}
Comment thread
daniel-lerch marked this conversation as resolved.
await emailDelivery.Enqueue(address.Address, preparedMessage, email.Id, cancellationToken);
}
email.DistributionListId = distributionList.Id;
Expand Down
43 changes: 18 additions & 25 deletions server/Mailist/EmailRelay/MimeMessageCreationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,53 +29,46 @@ public MimeMessageCreationService(IOptions<EmailRelayOptions> relayOptions, IOpt
}

/// <summary>
/// Prepends Resent headers to the original email in order to reintroduce it into the mail transport system.
/// Rewrites the original From header as Reply-To to forward a message in a DMARC compliant way.
/// </summary>
/// <param name="inboxEmail">The original email received via IMAP</param>
/// <param name="address">The recipient to deliver the original email to</param>
/// <returns>A complete MIME message which is ready to send</returns>
/// <remarks>
/// Forwarding emails by simply adding a Sender header fails DMARC policy checks because there is no valid DKIM signature of the original sender anymore.<br/>
/// To avoid this problem to we resent the entire MIME message: https://www.ietf.org/rfc/rfc2822.txt (Section 3.6.6)
/// To avoid this problem we tried to resent entire MIME messages: https://www.ietf.org/rfc/rfc2822.txt (Section 3.6.6)<br/>
/// For strict DMARC policies even this was not enough, because SPF and DKIM checks must pass on the From address.
/// </remarks>
Comment thread
daniel-lerch marked this conversation as resolved.
public MimeMessage PrepareForResentTo(InboxEmail inboxEmail, MailboxAddress address)
public async ValueTask<MimeMessage> PrepareForward(InboxEmail inboxEmail, CancellationToken cancellationToken)
{
if (inboxEmail.Header == null) throw new ArgumentNullException(nameof(inboxEmail), "inboxEmail.Header must not be null");
if (inboxEmail.Body == null) throw new ArgumentNullException(nameof(inboxEmail), "inboxEmail.Body must not be null");

HeaderList headers;
using (MemoryStream memoryStream = new(inboxEmail.Header))
headers = HeaderList.Load(memoryStream);
headers = HeaderList.Load(memoryStream, cancellationToken);

MimeEntity body;
using (MemoryStream memoryStream = new(inboxEmail.Body))
body = MimeEntity.Load(memoryStream);

headers.Insert(0, HeaderId.ResentFrom, new MailboxAddress(deliveryOptions.Value.SenderName, deliveryOptions.Value.SenderAddress).ToString());
headers.Insert(1, HeaderId.ResentTo, address.ToString());

return new MimeMessage(headers, body);
}

public async ValueTask<MimeMessage> PrepareForForwardTo(InboxEmail inboxEmail, MailboxAddress address, CancellationToken cancellationToken)
{
if (inboxEmail.Body == null) throw new ArgumentNullException(nameof(inboxEmail), "inboxEmail.Body must not be null");

MimeEntity body;
using (MemoryStream memoryStream = new(inboxEmail.Body))
// Reading from a MemoryStream is a synchronous operation that won't be cancelled anyhow
body = MimeEntity.Load(memoryStream, CancellationToken.None);
body = MimeEntity.Load(memoryStream, cancellationToken);

MailboxAddress? from = InternetAddressHelper.FirstMailboxAddressOrDefault(inboxEmail.From);
string? fromName = await FromName(from, cancellationToken);

if (!InternetAddressList.TryParse(inboxEmail.ReplyTo, out InternetAddressList? replyTo))
_ = InternetAddressList.TryParse(inboxEmail.From, out replyTo);

MimeMessage message = new();
message.From.Add(new MailboxAddress(fromName, deliveryOptions.Value.SenderAddress));
message.To.Add(address);
if (from != null)
message.ReplyTo.Add(from);
if (headers[HeaderId.Date] is { } date)
message.Headers.Add(HeaderId.Date, date);
if (inboxEmail.Subject != null)
message.Subject = inboxEmail.Subject;
message.From.Add(new MailboxAddress(fromName, deliveryOptions.Value.SenderAddress));
if (inboxEmail.To != null)
message.To.AddRange(InternetAddressList.Parse(inboxEmail.To));
if (headers[HeaderId.Cc] is { } cc)
message.Cc.AddRange(InternetAddressList.Parse(cc));
Comment thread
daniel-lerch marked this conversation as resolved.
if (replyTo != null)
message.ReplyTo.AddRange(replyTo);
message.Body = body;
return message;
}
Expand Down
Loading