Skip to content

Commit

Permalink
Added a work-around for Office365 message/delivery-status containing …
Browse files Browse the repository at this point in the history
…encoded status groups

Fixes a followup bug report in issue #250
  • Loading branch information
jstedfast committed Jan 27, 2022
1 parent e5ea952 commit a2826df
Show file tree
Hide file tree
Showing 5 changed files with 640 additions and 16 deletions.
67 changes: 57 additions & 10 deletions MimeKit/MessageDeliveryStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
using System;

using MimeKit.IO;
using MimeKit.IO.Filters;
using MimeKit.Utils;

namespace MimeKit {
/// <summary>
Expand Down Expand Up @@ -104,16 +106,7 @@ void CheckDisposed ()
Content = new MimeContent (new MemoryBlockStream ());
groups = new HeaderListCollection ();
} else {
groups = new HeaderListCollection ();

using (var stream = Content.Open ()) {
var parser = new MimeParser (stream, MimeFormat.Entity);

while (!parser.IsEndOfStream) {
var fields = parser.ParseHeaders ();
groups.Add (fields);
}
}
ParseStatusGroups ();
}

groups.Changed += OnGroupsChanged;
Expand All @@ -123,6 +116,60 @@ void CheckDisposed ()
}
}

void ParseStatusGroups ()
{
groups = new HeaderListCollection ();

try {
using (var stream = Content.Open ()) {
var parser = new MimeParser (stream, MimeFormat.Entity);
var encoding = ContentEncoding.Default;

// According to rfc3464, there are 1 or more Status Groups consisting of a block of field/value
// pairs (aka headers) separated by a blank line.
while (!parser.IsEndOfStream) {
var fields = parser.ParseHeaders ();
groups.Add (fields);

// Note: Office365 seems to sometimes base64 encode everything after the first Status Group of headers.
//
// In the sample case that @alex-jitbit provided in issue #250, Office365 added a Content-Transfer-Encoding
// header to the first Status Group and then base64 encoded the remainder of the content. Therefore, if we
// encounter a Content-Transfer-Encoding header (that needs decoding), break out of this loop so that we can
// decode the rest of the content and parse the result for the remainder of the Status Groups.
if (fields.TryGetHeader (HeaderId.ContentTransferEncoding, out var header)) {
MimeUtils.TryParse (header.Value, out encoding);

// Note: Base64, QuotedPrintable and UUEncode are all > Binary
if (encoding > ContentEncoding.Binary)
break;

// Note: If the Content-Transfer-Encoding is 7bit, 8bit, or even binary, then the content doesn't need to
// be decoded in order to continue parsing the remaining Status Groups.
encoding = ContentEncoding.Default;
}
}

if (encoding != ContentEncoding.Default) {
// This means that the remainder of the Status Groups have been encoded, so we'll need to decode
// the rest of the content stream in order to parse them.
using (var content = parser.ReadToEos ()) {
using (var filtered = new FilteredStream (content)) {
filtered.Add (DecoderFilter.Create (encoding));
parser.SetStream (filtered, MimeFormat.Entity);

while (!parser.IsEndOfStream) {
var fields = parser.ParseHeaders ();
groups.Add (fields);
}
}
}
}
}
} catch (ParseException) {
}
}

void OnGroupsChanged (object sender, EventArgs e)
{
var stream = new MemoryBlockStream ();
Expand Down
21 changes: 21 additions & 0 deletions MimeKit/MimeParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1686,6 +1686,27 @@ unsafe void ConstructMultipart (Multipart multipart, MimeEntityEndEventArgs args
boundary = BoundaryType.ImmediateBoundary;
}

/// <summary>
/// This is a hack needed by the MessageDeliveryStatus.ParseStatusGroups() logic in order to work around an Office365 bug.
/// </summary>
/// <returns>The remainder of the parser's input stream (needed because the input stream may not be seekable).</returns>
internal Stream ReadToEos ()
{
var content = new MemoryBlockStream ();

do {
if (ReadAhead (1, 0, CancellationToken.None) <= 0)
break;

content.Write (input, inputIndex, inputEnd - inputIndex);
inputIndex = inputEnd;
} while (!eos);

content.Position = 0;

return content;
}

unsafe HeaderList ParseHeaders (byte* inbuf, CancellationToken cancellationToken)
{
state = MimeParserState.Headers;
Expand Down
74 changes: 68 additions & 6 deletions UnitTests/MessageDeliveryStatusTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,18 @@ public void TestArgumentExceptions ()
}

[Test]
public void TestMimeParser ()
public void TestStatusGroups ()
{
var message = MimeMessage.Load (Path.Combine (TestHelper.ProjectDir, "TestData", "messages", "delivery-status.txt"));

Assert.IsInstanceOf<Multipart> (message.Body, "Expected top-level body part to be a multipart/report.");
Assert.IsInstanceOf<MultipartReport> (message.Body, "Expected top-level body part to be a multipart/report.");

var multipart = (Multipart) message.Body;
var report = (MultipartReport) message.Body;

Assert.IsInstanceOf<MessageDeliveryStatus> (multipart[0], "Expected first part to be a message/delivery-status.");
Assert.IsInstanceOf<MessageDeliveryStatus> (report[0], "Expected first part to be a message/delivery-status.");

var mds = (MessageDeliveryStatus) multipart[0];
var groups = mds.StatusGroups;
var delivery = (MessageDeliveryStatus) report[0];
var groups = delivery.StatusGroups;

Assert.IsNotNull (groups, "Did not expect null status groups.");
Assert.AreEqual (2, groups.Count, "Expected 2 groups of headers.");
Expand All @@ -70,6 +70,68 @@ public void TestMimeParser ()
Assert.AreEqual ("X-LOCAL; 500 (err.nosuchuser)", groups[1]["Diagnostic-Code"]);
}

// This tests issue #250
[Test]
public void TestStatusGroupsNoBlankLine ()
{
var message = MimeMessage.Load (Path.Combine (TestHelper.ProjectDir, "TestData", "messages", "delivery-status-no-blank-line.txt"));

Assert.IsInstanceOf<MultipartReport> (message.Body, "Expected top-level body part to be a multipart/report.");

var report = (MultipartReport) message.Body;

Assert.IsInstanceOf<MessageDeliveryStatus> (report[0], "Expected first part to be a message/delivery-status.");

var delivery = (MessageDeliveryStatus) report[0];
var groups = delivery.StatusGroups;

Assert.IsNotNull (groups, "Did not expect null status groups.");
Assert.AreEqual (2, groups.Count, "Expected 2 groups of headers.");

Assert.AreEqual ("dns; mm1", groups[0]["Reporting-MTA"]);
Assert.AreEqual ("Mon, 29 Jul 1996 02:12:50 -0700", groups[0]["Arrival-Date"]);

Assert.AreEqual ("RFC822; newsletter-request@imusic.com", groups[1]["Final-Recipient"]);
Assert.AreEqual ("failed", groups[1]["Action"]);
Assert.AreEqual ("X-LOCAL; 500 (err.nosuchuser)", groups[1]["Diagnostic-Code"]);
}

// This tests the bug that @alex-jitbit ran into in issue #250
[Test]
public void TestStatusGroupsWithContent ()
{
var message = MimeMessage.Load (Path.Combine (TestHelper.ProjectDir, "TestData", "messages", "bounce.txt"));

Assert.IsInstanceOf<MultipartReport> (message.Body, "Expected top-level body part to be a multipart/report.");

var report = (MultipartReport) message.Body;

Assert.IsInstanceOf<MessageDeliveryStatus> (report[1], "Expected second part to be a message/delivery-status.");

var delivery = (MessageDeliveryStatus) report[1];
Assert.AreEqual ("Delivery report", delivery.ContentDescription, "ContentDescription");
Assert.AreEqual ("934", delivery.Headers[HeaderId.ContentLength], "ContentLength");

var groups = delivery.StatusGroups;

Assert.IsNotNull (groups, "Did not expect null status groups.");
Assert.AreEqual (2, groups.Count, "Expected 2 groups of headers.");

Assert.AreEqual ("dns; hmail.jitbit.com", groups[0]["Reporting-MTA"]);
Assert.AreEqual ("630A242E63", groups[0]["X-Postfix-Queue-ID"]);
Assert.AreEqual ("rfc822; helpdesk@netecgc.com", groups[0]["X-Postfix-Sender"]);
Assert.AreEqual ("Wed, 26 Jan 2022 04:06:46 -0500 (EST)", groups[0]["Arrival-Date"]);
Assert.AreEqual ("base64", groups[0]["Content-Transfer-Encoding"]);
Assert.AreEqual ("712", groups[0]["Content-Length"]);

Assert.AreEqual ("rfc822; netec.test@netecgc.com", groups[1]["Final-Recipient"]);
Assert.AreEqual ("rfc822;netec.test@netecgc.com", groups[1]["Original-Recipient"]);
Assert.AreEqual ("failed", groups[1]["Action"]);
Assert.AreEqual ("5.1.1", groups[1]["Status"]);
Assert.AreEqual ("dns; https://urldefense.proofpoint.com/v2/url?u=http-3A__mx1-2Deu1.ppe-2Dhosted.com&d=DwICAQ&c=euGZstcaTDllvimEN8b7jXrwqOf-v5A_CdpgnVfiiMM&r=xGEu8UUVNHyj_BIRW7SVPK81Hnp-FSanq3-_T1am-Kg&m=RMniPmjTykiwdgbzUU7Cewy0BeD_osytuQLS6cflj30&s=0Q-rn8HZSqF10OISjAJdmdg7HT9iADG2jsaaaxtt7tE&e=", groups[1]["Remote-MTA"]);
Assert.AreEqual ("smtp; 550 5.1.1 <netec.test@netecgc.com>: Recipient address rejected: User unknown", groups[1]["Diagnostic-Code"]);
}

[Test]
public void TestSerializedContent ()
{
Expand Down
Loading

0 comments on commit a2826df

Please sign in to comment.