Skip to content

Commit

Permalink
Added a work-around for Exchange servers that send broken multipart B…
Browse files Browse the repository at this point in the history
…ODYSTRUCTURE responses
  • Loading branch information
jstedfast committed Sep 14, 2018
1 parent 63ef4db commit 7b33341
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 4 deletions.
18 changes: 14 additions & 4 deletions MailKit/Net/Imap/ImapUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -662,15 +662,25 @@ static async Task<object> ParseContentTypeAsync (ImapEngine engine, string forma
return contentType;
}

static async Task<ContentDisposition> ParseContentDispositionAsync (ImapEngine engine, string format, bool doAsync, CancellationToken cancellationToken)
static async Task<ContentDisposition> ParseContentDispositionAsync (ImapEngine engine, string format, bool isMultipart, bool doAsync, CancellationToken cancellationToken)
{
var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);

if (token.Type == ImapTokenType.Nil)
return null;

if (token.Type != ImapTokenType.OpenParen)
if (token.Type != ImapTokenType.OpenParen) {
if (isMultipart && token.Type == ImapTokenType.QString) {
// Note: This is a work-around for broken Exchange servers.
//
// See https://stackoverflow.com/questions/33481604/mailkit-fetch-unexpected-token-in-imap-response-qstring-multipart-message
// for details.
engine.Stream.UngetToken (token);
return null;
}

throw ImapEngine.UnexpectedToken (format, token);
}

var dsp = await ReadStringTokenAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false);

Expand Down Expand Up @@ -832,7 +842,7 @@ static async Task<BodyPart> ParseMultipartAsync (ImapEngine engine, string forma
}

if (token.Type != ImapTokenType.CloseParen) {
body.ContentDisposition = await ParseContentDispositionAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false);
body.ContentDisposition = await ParseContentDispositionAsync (engine, format, true, doAsync, cancellationToken).ConfigureAwait (false);
token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
}

Expand Down Expand Up @@ -938,7 +948,7 @@ public static async Task<BodyPart> ParseBodyAsync (ImapEngine engine, string for
}

if (token.Type != ImapTokenType.CloseParen) {
body.ContentDisposition = await ParseContentDispositionAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false);
body.ContentDisposition = await ParseContentDispositionAsync (engine, format, false, doAsync, cancellationToken).ConfigureAwait (false);
token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
}

Expand Down
52 changes: 52 additions & 0 deletions UnitTests/Net/Imap/ImapUtilsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,58 @@ public void TestParseMultipartBodyStructureWithNilBodyFldParam ()
}
}

[Test]
public void TestParseMultipartBodyStructureWithoutBodyFldDsp ()
{
// Test case from https://stackoverflow.com/questions/33481604/mailkit-fetch-unexpected-token-in-imap-response-qstring-multipart-message
const string text = "((\"text\" \"plain\" (\"charset\" \"UTF-8\") NIL \"Message text\" \"Quoted-printable\" 209 6 NIL (\"inline\" NIL) NIL NIL)(\"text\" \"xml\" (\"name\" \"4441004299066.xml\") NIL \"4441004299066.xml\" \"Base64\" 10642 137 NIL (\"inline\" (\"filename\" \"4441004299066.xml\")) NIL NIL)(\"application\" \"pdf\" (\"name\" \"4441004299066.pdf\") NIL \"4441004299066.pdf\" \"Base64\" 48448 NIL (\"inline\" (\"filename\" \"4441004299066.pdf\")) NIL NIL) \"mixed\" (\"boundary\" \"6624CFB2_17170C36_Synapse_boundary\") \"Multipart message\" NIL)\r\n";

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

engine.SetStream (tokenizer);

try {
body = ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, false, CancellationToken.None).GetAwaiter ().GetResult ();
} catch (Exception ex) {
Assert.Fail ("Parsing BODYSTRUCTURE failed: {0}", ex);
return;
}

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

Assert.IsInstanceOf<BodyPartMultipart> (body, "Body types did not match.");
multipart = (BodyPartMultipart) body;

Assert.IsTrue (body.ContentType.IsMimeType ("multipart", "mixed"), "Content-Type did not match.");
Assert.AreEqual ("6624CFB2_17170C36_Synapse_boundary", body.ContentType.Parameters ["boundary"], "boundary param did not match");
Assert.AreEqual (3, multipart.BodyParts.Count, "BodyParts count does not match.");

Assert.IsInstanceOf<BodyPartText> (multipart.BodyParts[0], "The type of the first child does not match.");
basic = (BodyPartBasic) multipart.BodyParts[0];
Assert.AreEqual ("plain", basic.ContentType.MediaSubtype, "Content-Type did not match.");
Assert.AreEqual ("Message text", basic.ContentDescription, "Content-Description does not match.");

Assert.IsInstanceOf<BodyPartText> (multipart.BodyParts[1], "The type of the second child does not match.");
basic = (BodyPartBasic) multipart.BodyParts[1];
Assert.AreEqual ("xml", basic.ContentType.MediaSubtype, "Content-Type did not match.");
Assert.AreEqual ("4441004299066.xml", basic.ContentDescription, "Content-Description does not match.");

Assert.IsInstanceOf<BodyPartBasic> (multipart.BodyParts[2], "The type of the third child does not match.");
basic = (BodyPartBasic) multipart.BodyParts[2];
Assert.AreEqual ("application", basic.ContentType.MediaType, "Content-Type did not match.");
Assert.AreEqual ("pdf", basic.ContentType.MediaSubtype, "Content-Type did not match.");
Assert.AreEqual ("4441004299066.pdf", basic.ContentDescription, "Content-Description does not match.");
}
}
}
}

[Test]
public void TestParseExampleThreads ()
{
Expand Down

0 comments on commit 7b33341

Please sign in to comment.