Skip to content

Commit

Permalink
Added a workaround for GMail BODY responses that include `("ALTERNATI…
Browse files Browse the repository at this point in the history
…VE")`

Similar workaround to BODYSTRUCTURE responses that we already had.

Fixes issue #1841
  • Loading branch information
jstedfast committed Nov 29, 2024
1 parent 200a1ff commit 3d7e35e
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 10 deletions.
30 changes: 20 additions & 10 deletions MailKit/Net/Imap/ImapUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1696,20 +1696,25 @@ static bool ShouldParseMultipart (ImapEngine engine, CancellationToken cancellat
if (engine.QuirksMode == ImapQuirksMode.GMail && token.Type != ImapTokenType.Literal) {
// Note: GMail's IMAP server implementation breaks when it encounters nested multiparts with the same
// boundary and returns a BODYSTRUCTURE like the example in https://github.com/jstedfast/MailKit/issues/205
// or like the example in https://github.com/jstedfast/MailKit/issues/777:
// or like the example in https://github.com/jstedfast/MailKit/issues/777. There's also an issue with BODY
// responses like the one in https://github.com/jstedfast/MailKit/issues/1841.
//
// ("ALTERNATIVE" ("BOUNDARY" "==alternative_xad5934455aeex") NIL NIL)
// or
// ("RELATED" NIL ("ATTACHMENT" NIL) NIL)
// or
// ("ALTERNATIVE")
//
// Check if the next token is either a '(', ')' or NIL.
//
// Check if the next token is either a '(' or NIL. If it is '(', then that would indicate the start of
// the Content-Type parameter list. If it is NIL, then it would signify that the Content-Type has no
// parameters.
// If it is '(', then that would indicate the start of the Content-Type parameter list.
// If it is ')', then that would indicate a BODY response without a Content-Type parameter list.
// If it is NIL, then it would signify that the Content-Type has no parameters.

// Peek at the next token to see what we've got. If we get a '(' or NIL, then treat this as a multipart.
nextToken = engine.PeekToken (cancellationToken);

if (nextToken.Type == ImapTokenType.OpenParen || nextToken.Type == ImapTokenType.Nil) {
if (nextToken.Type == ImapTokenType.OpenParen || nextToken.Type == ImapTokenType.CloseParen || nextToken.Type == ImapTokenType.Nil) {
// Unget the multipart subtype.
engine.Stream.UngetToken (token);

Expand Down Expand Up @@ -1778,20 +1783,25 @@ static async Task<bool> ShouldParseMultipartAsync (ImapEngine engine, Cancellati
if (engine.QuirksMode == ImapQuirksMode.GMail && token.Type != ImapTokenType.Literal) {
// Note: GMail's IMAP server implementation breaks when it encounters nested multiparts with the same
// boundary and returns a BODYSTRUCTURE like the example in https://github.com/jstedfast/MailKit/issues/205
// or like the example in https://github.com/jstedfast/MailKit/issues/777:
// or like the example in https://github.com/jstedfast/MailKit/issues/777. There's also an issue with BODY
// responses like the one in https://github.com/jstedfast/MailKit/issues/1841.
//
// ("ALTERNATIVE" ("BOUNDARY" "==alternative_xad5934455aeex") NIL NIL)
// or
// ("RELATED" NIL ("ATTACHMENT" NIL) NIL)
// or
// ("ALTERNATIVE")
//
// Check if the next token is either a '(', ')' or NIL.
//
// Check if the next token is either a '(' or NIL. If it is '(', then that would indicate the start of
// the Content-Type parameter list. If it is NIL, then it would signify that the Content-Type has no
// parameters.
// If it is '(', then that would indicate the start of the Content-Type parameter list.
// If it is ')', then that would indicate a BODY response without a Content-Type parameter list.
// If it is NIL, then it would signify that the Content-Type has no parameters.

// Peek at the next token to see what we've got. If we get a '(' or NIL, then treat this as a multipart.
nextToken = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false);

if (nextToken.Type == ImapTokenType.OpenParen || nextToken.Type == ImapTokenType.Nil) {
if (nextToken.Type == ImapTokenType.OpenParen || nextToken.Type == ImapTokenType.CloseParen || nextToken.Type == ImapTokenType.Nil) {
// Unget the multipart subtype.
engine.Stream.UngetToken (token);

Expand Down
120 changes: 120 additions & 0 deletions UnitTests/Net/Imap/ImapUtilsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3732,6 +3732,126 @@ public async Task TestParseBadlyFormedBodyStructureWithEmptyParensInsteadOfConte
}
}

static void AssertParseBadlyFormedGMailMultipartBodyResponseWithNoChildren (BodyPart body)
{
Assert.That (body, Is.InstanceOf<BodyPartMultipart> (), "Body types did not match.");
var multipart = (BodyPartMultipart) body;

Assert.That (multipart.ContentType.IsMimeType ("multipart", "report"), Is.True, "Content-Type did not match.");
Assert.That (multipart.ContentType.Boundary, Is.Null, "boundary should be null");
Assert.That (multipart.ContentDisposition, Is.Null, "Content-Disposition should be null");
Assert.That (multipart.ContentLanguage, Is.Null, "Content-Language should be null");
Assert.That (multipart.ContentLocation, Is.Null, "Content-Location should be null");
Assert.That (multipart.BodyParts, Has.Count.EqualTo (3), "multipart children did not match");

Assert.That (multipart.BodyParts[0], Is.InstanceOf<BodyPartText> (), "First multipart/report subpart types did not match.");
var text = (BodyPartText) multipart.BodyParts[0];
Assert.That (text.ContentType.IsMimeType ("text", "plain"), Is.True, "Content-Type did not match.");
Assert.That (text.ContentType.Charset, Is.EqualTo ("windows-1252"), "Charset param did not match");
Assert.That (text.ContentDescription, Is.Null, "Content-Description should be null");
Assert.That (text.ContentDisposition, Is.Null, "Content-Disposition should be null");
Assert.That (text.ContentId, Is.Null, "Content-Id should be null");
Assert.That (text.ContentLanguage, Is.Null, "Content-Language should be null");
Assert.That (text.ContentLocation, Is.Null, "Content-Location should be null");
Assert.That (text.ContentMd5, Is.Null, "Content-Md5 should be null");
Assert.That (text.ContentTransferEncoding, Is.EqualTo ("QUOTED-PRINTABLE"), "Content-Transfer-Encodings did not match");
Assert.That (text.Octets, Is.EqualTo (211), "Octets did not match");
Assert.That (text.Lines, Is.EqualTo (5), "Lines did not match");

Assert.That (multipart.BodyParts[1], Is.InstanceOf<BodyPartBasic> (), "Second multipart/report subpart types did not match.");
var deliveryStatus = (BodyPartBasic) multipart.BodyParts[1];
Assert.That (deliveryStatus.ContentType.IsMimeType ("message", "delivery-status"), Is.True, "Content-Type did not match.");
Assert.That (deliveryStatus.ContentDescription, Is.Null, "Content-Description should be null");
Assert.That (deliveryStatus.ContentDisposition, Is.Null, "Content-Disposition should be null");
Assert.That (deliveryStatus.ContentId, Is.Null, "Content-Id should be null");
Assert.That (deliveryStatus.ContentLanguage, Is.Null, "Content-Language should be null");
Assert.That (deliveryStatus.ContentLocation, Is.Null, "Content-Location should be null");
Assert.That (deliveryStatus.ContentMd5, Is.Null, "Content-Md5 should be null");
Assert.That (deliveryStatus.ContentTransferEncoding, Is.EqualTo ("7BIT"), "Content-Transfer-Encodings did not match");
Assert.That (deliveryStatus.Octets, Is.EqualTo (344), "Octets did not match");

Assert.That (multipart.BodyParts[2], Is.InstanceOf<BodyPartMessage> (), "Third multipart/report subpart types did not match.");
var rfc822 = (BodyPartMessage) multipart.BodyParts[2];
Assert.That (rfc822.ContentType.IsMimeType ("message", "rfc822"), Is.True, "Content-Type did not match.");
Assert.That (rfc822.ContentDescription, Is.Null, "Content-Description should be null");
Assert.That (rfc822.ContentDisposition, Is.Null, "Content-Disposition should be null");
Assert.That (rfc822.ContentId, Is.Null, "Content-Id should be null");
Assert.That (rfc822.ContentLanguage, Is.Null, "Content-Language should be null");
Assert.That (rfc822.ContentLocation, Is.Null, "Content-Location should be null");
Assert.That (rfc822.ContentMd5, Is.Null, "Content-Md5 should be null");
Assert.That (rfc822.ContentTransferEncoding, Is.EqualTo ("7BIT"), "Content-Transfer-Encodings did not match");
Assert.That (rfc822.Octets, Is.EqualTo (2942), "Octets did not match");

Assert.That (rfc822.Body, Is.InstanceOf<BodyPartMultipart> (), "Body of message/rfc822 did not match.");
var alternative = (BodyPartMultipart) rfc822.Body;
Assert.That (alternative.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "Content-Type did not match.");
Assert.That (alternative.ContentType.Boundary, Is.Null, "boundary should be null");
Assert.That (alternative.ContentDisposition, Is.Null, "Content-Disposition should be null");
Assert.That (alternative.ContentLanguage, Is.Null, "Content-Language should be null");
Assert.That (alternative.ContentLocation, Is.Null, "Content-Location should be null");
Assert.That (alternative.BodyParts, Has.Count.EqualTo (0), "multipart children did not match");
}

// issue 1841
[Test]
public void TestParseBadlyFormedGMailMultipartBodyResponseWithNoChildren ()
{
const string text = "((\"TEXT\" \"PLAIN\" (\"CHARSET\" \"windows-1252\") NIL NIL \"QUOTED-PRINTABLE\" 211 5)(\"MESSAGE\" \"DELIVERY-STATUS\" NIL NIL NIL \"7BIT\" 344)(\"MESSAGE\" \"RFC822\" NIL NIL NIL \"7BIT\" 2942 (\"Sat, 3 Jan 2015 19:26:15 +0100\" \"Re: MPG\" ((\"c p\" NIL \"---.---87\" \"gmail.com\")) ((\"c p\" NIL \"---.---87\" \"gmail.com\")) ((\"c p\" NIL \"---.---87\" \"gmail.com\")) ((\"am sa[cft]\" NIL \"sm\" \"cft.ce.fr\")) NIL NIL \"<D3CAD328B4CFD24E83DD07E8923DFEE902FA8F1B@PRDGCE8P4523.sigce.ce.fr>\" \"<[email protected]>\") (\"ALTERNATIVE\") 51) \"REPORT\")\r\n";

using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) {
using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) {
using (var engine = new ImapEngine (null)) {
BodyPart body;

engine.QuirksMode = ImapQuirksMode.GMail;
engine.SetStream (tokenizer);

try {
body = ImapUtils.ParseBody (engine, "Syntax error in BODY: {0}", string.Empty, CancellationToken.None);
} catch (Exception ex) {
Assert.Fail ($"Parsing BODY failed: {ex}");
return;
}

var token = engine.ReadToken (CancellationToken.None);
Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}");

AssertParseBadlyFormedGMailMultipartBodyResponseWithNoChildren (body);
}
}
}
}

// issue 1841
[Test]
public async Task TestParseBadlyFormedGMailMultipartBodyResponseWithNoChildrenAsync ()
{
const string text = "((\"TEXT\" \"PLAIN\" (\"CHARSET\" \"windows-1252\") NIL NIL \"QUOTED-PRINTABLE\" 211 5)(\"MESSAGE\" \"DELIVERY-STATUS\" NIL NIL NIL \"7BIT\" 344)(\"MESSAGE\" \"RFC822\" NIL NIL NIL \"7BIT\" 2942 (\"Sat, 3 Jan 2015 19:26:15 +0100\" \"Re: MPG\" ((\"c p\" NIL \"---.---87\" \"gmail.com\")) ((\"c p\" NIL \"---.---87\" \"gmail.com\")) ((\"c p\" NIL \"---.---87\" \"gmail.com\")) ((\"am sa[cft]\" NIL \"sm\" \"cft.ce.fr\")) NIL NIL \"<D3CAD328B4CFD24E83DD07E8923DFEE902FA8F1B@PRDGCE8P4523.sigce.ce.fr>\" \"<[email protected]>\") (\"ALTERNATIVE\") 51) \"REPORT\")\r\n";

using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) {
using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) {
using (var engine = new ImapEngine (null)) {
BodyPart body;

engine.QuirksMode = ImapQuirksMode.GMail;
engine.SetStream (tokenizer);

try {
body = await ImapUtils.ParseBodyAsync (engine, "Syntax error in BODY: {0}", string.Empty, CancellationToken.None);
} catch (Exception ex) {
Assert.Fail ($"Parsing BODY failed: {ex}");
return;
}

var token = await engine.ReadTokenAsync (CancellationToken.None);
Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}");

AssertParseBadlyFormedGMailMultipartBodyResponseWithNoChildren (body);
}
}
}
}

[Test]
public void TestParseExampleThreads ()
{
Expand Down

0 comments on commit 3d7e35e

Please sign in to comment.