Skip to content

Commit 4cb236a

Browse files
committed
Modified MimeReader/ExperimentalMimeParser to handle "double boundaries"
Also updated HeaderList to track whether or not it should write a body separator (i.e. a blank line to separate the headers from the body). This gets a bunch more ExperimentalMimeParserTests to pass.
1 parent 01c6e00 commit 4cb236a

11 files changed

+327
-153
lines changed

MimeKit/AsyncMimeParser.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ async Task ConstructMessagePartAsync (MessagePart rfc822, MimeEntityEndEventArgs
331331
}
332332

333333
var type = GetContentType (null);
334-
var entity = options.CreateEntity (type, headers, true, depth);
334+
var entity = options.CreateEntity (type, headers, hasBodySeparator: true, toplevel: true, depth);
335335
var entityArgs = new MimeEntityEndEventArgs (entity) {
336336
HeadersEndOffset = headerBlockEnd,
337337
BeginOffset = headerBlockBegin,
@@ -425,7 +425,7 @@ async Task MultipartScanSubpartsAsync (Multipart multipart, int depth, Cancellat
425425
// return BoundaryType.EndBoundary;
426426

427427
var type = GetContentType (multipart.ContentType);
428-
var entity = options.CreateEntity (type, headers, false, depth);
428+
var entity = options.CreateEntity (type, headers, hasBodySeparator: true, toplevel: false, depth);
429429
var entityArgs = new MimeEntityEndEventArgs (entity, multipart) {
430430
HeadersEndOffset = headerBlockEnd,
431431
BeginOffset = headerBlockBegin,
@@ -532,7 +532,7 @@ public async Task<HeaderList> ParseHeadersAsync (CancellationToken cancellationT
532532

533533
state = eos ? MimeParserState.Eos : MimeParserState.Complete;
534534

535-
var parsed = new HeaderList (options);
535+
var parsed = new HeaderList (options, true);
536536
foreach (var header in headers)
537537
parsed.Add (header);
538538

@@ -576,7 +576,7 @@ public async Task<MimeEntity> ParseEntityAsync (CancellationToken cancellationTo
576576

577577
// Note: we pass 'false' as the 'toplevel' argument here because
578578
// we want the entity to consume all the headers.
579-
var entity = options.CreateEntity (type, headers, false, 0);
579+
var entity = options.CreateEntity (type, headers, hasBodySeparator: true, toplevel: false, 0);
580580
var entityArgs = new MimeEntityEndEventArgs (entity) {
581581
HeadersEndOffset = headerBlockEnd,
582582
BeginOffset = headerBlockBegin,
@@ -676,7 +676,7 @@ public async Task<MimeMessage> ParseMessageAsync (CancellationToken cancellation
676676
}
677677

678678
var type = GetContentType (null);
679-
var entity = options.CreateEntity (type, headers, true, 0);
679+
var entity = options.CreateEntity (type, headers, hasBodySeparator: true, toplevel: true, 0);
680680
var entityArgs = new MimeEntityEndEventArgs (entity) {
681681
HeadersEndOffset = headerBlockEnd,
682682
BeginOffset = headerBlockBegin,

MimeKit/AsyncMimeReader.cs

+1-15
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ async Task StepHeadersAsync (CancellationToken cancellationToken)
173173

174174
// Check for an empty line denoting the end of the header block.
175175
if (IsEndOfHeaderBlock (left)) {
176+
await OnBodySeparatorAsync (beginOffset, beginLineNumber, GetOffset (inputIndex), CancellationToken.None).ConfigureAwait (false);
176177
state = MimeParserState.Content;
177178
break;
178179
}
@@ -547,21 +548,6 @@ async Task MultipartScanSubpartsAsync (ContentType multipartContentType, int dep
547548
state = MimeParserState.Headers;
548549
await StepAsync (cancellationToken).ConfigureAwait (false);
549550

550-
if (state == MimeParserState.Boundary) {
551-
if (headerCount == 0) {
552-
if (boundary == BoundaryType.ImmediateBoundary)
553-
continue;
554-
555-
return;
556-
}
557-
558-
// This part has no content, but that will be handled in ConstructMultipartAsync()
559-
// or ConstructMimePartAsync().
560-
}
561-
562-
//if (state == ParserState.Complete && headers.Count == 0)
563-
// return BoundaryType.EndBoundary;
564-
565551
var type = GetContentType (multipartContentType);
566552
var currentHeadersEndOffset = headerBlockEnd;
567553
var currentBeginOffset = headerBlockBegin;

MimeKit/ExperimentalMimeParser.cs

+23-5
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public class ExperimentalMimeParser : MimeReader, IMimeParser
6363
Stream content;
6464

6565
bool parsingMessageHeaders;
66+
bool hasBodySeparator;
6667
int depth;
6768

6869
bool persistent;
@@ -335,6 +336,7 @@ protected override void OnHeadersBegin (long beginOffset, int beginLineNumber, C
335336
{
336337
headers.Clear ();
337338
preHeaderLength = 0;
339+
hasBodySeparator = false;
338340
}
339341

340342
/// <summary>
@@ -378,6 +380,22 @@ protected override void OnHeadersEnd (long beginOffset, int beginLineNumber, lon
378380
parsingMessageHeaders = false;
379381
}
380382

383+
/// <summary>
384+
/// Called when the body separator is encountered in the stream.
385+
/// </summary>
386+
/// <remarks>
387+
/// <para>Called when the body separator is encountered in the stream.</para>
388+
/// <para>This method is always called before <see cref="OnHeadersEnd"/> if a body separator is found.</para>
389+
/// </remarks>
390+
/// <param name="beginOffset">The offset into the stream where the body separator began.</param>
391+
/// <param name="lineNumber">The line number where the body separator was found.</param>
392+
/// <param name="endOffset">The offset into the stream where the body separator ended.</param>
393+
/// <param name="cancellationToken">The cancellation token.</param>
394+
protected override void OnBodySeparator (long beginOffset, int lineNumber, long endOffset, CancellationToken cancellationToken)
395+
{
396+
hasBodySeparator = true;
397+
}
398+
381399
#endregion Header Events
382400

383401
#region MimeMessage Events
@@ -447,7 +465,7 @@ protected override void OnMimeMessageEnd (long beginOffset, int beginLineNumber,
447465
protected override void OnMimePartBegin (ContentType contentType, long beginOffset, int beginLineNumber, CancellationToken cancellationToken)
448466
{
449467
var toplevel = stack.Count > 0 && stack.Peek () is MimeMessage;
450-
var part = Options.CreateEntity (contentType, headers, toplevel, depth);
468+
var part = Options.CreateEntity (contentType, headers, hasBodySeparator, toplevel, depth);
451469

452470
PushEntity (part);
453471
}
@@ -555,7 +573,7 @@ protected override void OnMimePartEnd (ContentType contentType, long beginOffset
555573
protected override void OnMessagePartBegin (ContentType contentType, long beginOffset, int beginLineNumber, CancellationToken cancellationToken)
556574
{
557575
var toplevel = stack.Count > 0 && stack.Peek () is MimeMessage;
558-
var rfc822 = Options.CreateEntity (contentType, headers, toplevel, depth);
576+
var rfc822 = Options.CreateEntity (contentType, headers, hasBodySeparator, toplevel, depth);
559577

560578
parsingMessageHeaders = true;
561579
PushEntity (rfc822);
@@ -600,7 +618,7 @@ protected override void OnMessagePartEnd (ContentType contentType, long beginOff
600618
protected override void OnMultipartBegin (ContentType contentType, long beginOffset, int beginLineNumber, CancellationToken cancellationToken)
601619
{
602620
var toplevel = stack.Count > 0 && stack.Peek () is MimeMessage;
603-
var multipart = Options.CreateEntity (contentType, headers, toplevel, depth);
621+
var multipart = Options.CreateEntity (contentType, headers, hasBodySeparator, toplevel, depth);
604622

605623
PushEntity (multipart);
606624
depth++;
@@ -838,7 +856,7 @@ public HeaderList ParseHeaders (CancellationToken cancellationToken = default)
838856
throw;
839857
}
840858

841-
var parsed = new HeaderList (Options);
859+
var parsed = new HeaderList (Options, hasBodySeparator);
842860
foreach (var header in headers)
843861
parsed.Add (header);
844862

@@ -873,7 +891,7 @@ public async Task<HeaderList> ParseHeadersAsync (CancellationToken cancellationT
873891
throw;
874892
}
875893

876-
var parsed = new HeaderList (Options);
894+
var parsed = new HeaderList (Options, hasBodySeparator);
877895
foreach (var header in headers)
878896
parsed.Add (header);
879897

MimeKit/HeaderList.cs

+10-2
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,12 @@ public sealed class HeaderList : IList<Header>
5050
// this table references the first header of each field
5151
readonly Dictionary<string, Header> table;
5252
readonly List<Header> headers;
53+
bool hasBodySeparator;
5354

54-
internal HeaderList (ParserOptions options)
55+
internal HeaderList (ParserOptions options, bool hasBodySeparator)
5556
{
5657
table = new Dictionary<string, Header> (MimeUtils.OrdinalIgnoreCase);
58+
this.hasBodySeparator = hasBodySeparator;
5759
headers = new List<Header> ();
5860
Options = options;
5961
}
@@ -64,7 +66,7 @@ internal HeaderList (ParserOptions options)
6466
/// <remarks>
6567
/// Creates a new empty header list.
6668
/// </remarks>
67-
public HeaderList () : this (ParserOptions.Default.Clone ())
69+
public HeaderList () : this (ParserOptions.Default.Clone (), true)
6870
{
6971
}
7072

@@ -704,6 +706,9 @@ public void WriteTo (FormatOptions options, Stream stream, CancellationToken can
704706
filtered.Flush (cancellationToken);
705707
}
706708

709+
if (!hasBodySeparator)
710+
return;
711+
707712
if (stream is ICancellableStream cancellable) {
708713
cancellable.Write (options.NewLineBytes, 0, options.NewLineBytes.Length, cancellationToken);
709714
} else {
@@ -758,6 +763,9 @@ public async Task WriteToAsync (FormatOptions options, Stream stream, Cancellati
758763
await filtered.FlushAsync (cancellationToken).ConfigureAwait (false);
759764
}
760765

766+
if (!hasBodySeparator)
767+
return;
768+
761769
await stream.WriteAsync (options.NewLineBytes, 0, options.NewLineBytes.Length, cancellationToken).ConfigureAwait (false);
762770
}
763771

MimeKit/MimeEntity.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ protected MimeEntity (MimeEntityConstructorArgs args)
9292
if (args is null)
9393
throw new ArgumentNullException (nameof (args));
9494

95-
Headers = new HeaderList (args.ParserOptions);
95+
Headers = new HeaderList (args.ParserOptions, args.HasBodySeparator);
9696
ContentType = args.ContentType;
9797

9898
foreach (var header in args.Headers) {

MimeKit/MimeEntityConstructorArgs.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,12 @@ public sealed class MimeEntityConstructorArgs
3838
internal readonly ParserOptions ParserOptions;
3939
internal readonly IEnumerable<Header> Headers;
4040
internal readonly ContentType ContentType;
41+
internal readonly bool HasBodySeparator;
4142
internal readonly bool IsTopLevel;
4243

43-
internal MimeEntityConstructorArgs (ParserOptions options, ContentType ctype, IEnumerable<Header> headers, bool toplevel)
44+
internal MimeEntityConstructorArgs (ParserOptions options, ContentType ctype, IEnumerable<Header> headers, bool hasBodySeparator, bool toplevel)
4445
{
46+
HasBodySeparator = hasBodySeparator;
4547
ParserOptions = options;
4648
IsTopLevel = toplevel;
4749
ContentType = ctype;

MimeKit/MimeMessage.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ enum LazyLoadedFields {
107107
internal MimeMessage (ParserOptions options, IEnumerable<Header> headers, RfcComplianceMode mode)
108108
{
109109
addresses = new Dictionary<HeaderId, InternetAddressList> ();
110-
Headers = new HeaderList (options);
110+
Headers = new HeaderList (options, true);
111111

112112
compliance = mode;
113113

@@ -135,7 +135,7 @@ internal MimeMessage (ParserOptions options, IEnumerable<Header> headers, RfcCom
135135
internal MimeMessage (ParserOptions options)
136136
{
137137
addresses = new Dictionary<HeaderId, InternetAddressList> ();
138-
Headers = new HeaderList (options);
138+
Headers = new HeaderList (options, true);
139139

140140
compliance = RfcComplianceMode.Strict;
141141

@@ -252,7 +252,7 @@ public MimeMessage (IEnumerable<Header> headers)
252252
if (headers is HeaderList headerList) {
253253
Headers = headerList;
254254
} else {
255-
Headers = new HeaderList (ParserOptions.Default.Clone ());
255+
Headers = new HeaderList (ParserOptions.Default.Clone (), true);
256256

257257
foreach (var header in headers)
258258
Headers.Add (header);

MimeKit/MimeParser.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -1543,7 +1543,7 @@ unsafe void ConstructMessagePart (MessagePart rfc822, MimeEntityEndEventArgs arg
15431543
}
15441544

15451545
var type = GetContentType (null);
1546-
var entity = options.CreateEntity (type, headers, true, depth);
1546+
var entity = options.CreateEntity (type, headers, hasBodySeparator: true, toplevel: true, depth);
15471547
var entityArgs = new MimeEntityEndEventArgs (entity) {
15481548
HeadersEndOffset = headerBlockEnd,
15491549
BeginOffset = headerBlockBegin,
@@ -1637,7 +1637,7 @@ unsafe void MultipartScanSubparts (Multipart multipart, byte* inbuf, int depth,
16371637
// return BoundaryType.EndBoundary;
16381638

16391639
var type = GetContentType (multipart.ContentType);
1640-
var entity = options.CreateEntity (type, headers, false, depth);
1640+
var entity = options.CreateEntity (type, headers, hasBodySeparator: true, toplevel: false, depth);
16411641
var entityArgs = new MimeEntityEndEventArgs (entity, multipart) {
16421642
HeadersEndOffset = headerBlockEnd,
16431643
BeginOffset = headerBlockBegin,
@@ -1779,7 +1779,7 @@ unsafe HeaderList ParseHeaders (byte* inbuf, CancellationToken cancellationToken
17791779

17801780
state = eos ? MimeParserState.Eos : MimeParserState.Complete;
17811781

1782-
var parsed = new HeaderList (options);
1782+
var parsed = new HeaderList (options, true);
17831783
foreach (var header in headers)
17841784
parsed.Add (header);
17851785

@@ -1878,7 +1878,7 @@ unsafe MimeEntity ParseEntity (byte* inbuf, CancellationToken cancellationToken)
18781878

18791879
// Note: we pass 'false' as the 'toplevel' argument here because
18801880
// we want the entity to consume all the headers.
1881-
var entity = options.CreateEntity (type, headers, false, 0);
1881+
var entity = options.CreateEntity (type, headers, hasBodySeparator: true, toplevel: false, 0);
18821882
var entityArgs = new MimeEntityEndEventArgs (entity) {
18831883
HeadersEndOffset = headerBlockEnd,
18841884
BeginOffset = headerBlockBegin,
@@ -1986,7 +1986,7 @@ unsafe MimeMessage ParseMessage (byte* inbuf, CancellationToken cancellationToke
19861986
}
19871987

19881988
var type = GetContentType (null);
1989-
var entity = options.CreateEntity (type, headers, true, 0);
1989+
var entity = options.CreateEntity (type, headers, hasBodySeparator: true, toplevel: true, 0);
19901990
var entityArgs = new MimeEntityEndEventArgs (entity) {
19911991
HeadersEndOffset = headerBlockEnd,
19921992
BeginOffset = headerBlockBegin,

MimeKit/MimeReader.cs

+34-15
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,39 @@ protected virtual Task OnHeadersEndAsync (long beginOffset, int beginLineNumber,
390390
return Task.CompletedTask;
391391
}
392392

393+
/// <summary>
394+
/// Called when the body separator is encountered in the stream.
395+
/// </summary>
396+
/// <remarks>
397+
/// <para>Called when the body separator is encountered in the stream.</para>
398+
/// <para>This method is always called before <see cref="OnHeadersEnd"/> if a body separator is found.</para>
399+
/// </remarks>
400+
/// <param name="beginOffset">The offset into the stream where the body separator began.</param>
401+
/// <param name="lineNumber">The line number where the body separator was found.</param>
402+
/// <param name="endOffset">The offset into the stream where the body separator ended.</param>
403+
/// <param name="cancellationToken">The cancellation token.</param>
404+
protected virtual void OnBodySeparator (long beginOffset, int lineNumber, long endOffset, CancellationToken cancellationToken)
405+
{
406+
}
407+
408+
/// <summary>
409+
/// Called when the body separator is encountered in the stream.
410+
/// </summary>
411+
/// <remarks>
412+
/// <para>Called when the body separator is encountered in the stream.</para>
413+
/// <para>This method is always called before <see cref="OnHeadersEndAsync"/> if a body separator is found.</para>
414+
/// </remarks>
415+
/// <returns>An asynchronous task context.</returns>
416+
/// <param name="beginOffset">The offset into the stream where the body separator began.</param>
417+
/// <param name="lineNumber">The line number where the body separator was found.</param>
418+
/// <param name="endOffset">The offset into the stream where the body separator ended.</param>
419+
/// <param name="cancellationToken">The cancellation token.</param>
420+
protected virtual Task OnBodySeparatorAsync (long beginOffset, int lineNumber, long endOffset, CancellationToken cancellationToken)
421+
{
422+
OnBodySeparator (beginOffset, lineNumber, endOffset, cancellationToken);
423+
return Task.CompletedTask;
424+
}
425+
393426
#endregion Header Events
394427

395428
#region MimeMessage Events
@@ -1860,6 +1893,7 @@ unsafe void StepHeaders (byte* inbuf, CancellationToken cancellationToken)
18601893

18611894
// Check for an empty line denoting the end of the header block.
18621895
if (IsEndOfHeaderBlock (left)) {
1896+
OnBodySeparator (beginOffset, beginLineNumber, GetOffset (inputIndex), CancellationToken.None);
18631897
state = MimeParserState.Content;
18641898
break;
18651899
}
@@ -2474,21 +2508,6 @@ unsafe void MultipartScanSubparts (ContentType multipartContentType, byte* inbuf
24742508
state = MimeParserState.Headers;
24752509
Step (inbuf, cancellationToken);
24762510

2477-
if (state == MimeParserState.Boundary) {
2478-
if (headerCount == 0) {
2479-
if (boundary == BoundaryType.ImmediateBoundary)
2480-
continue;
2481-
2482-
return;
2483-
}
2484-
2485-
// This part has no content, but that will be handled in ConstructMultipart()
2486-
// or ConstructMimePart().
2487-
}
2488-
2489-
//if (state == ParserState.Complete && headers.Count == 0)
2490-
// return BoundaryType.EndBoundary;
2491-
24922511
var type = GetContentType (multipartContentType);
24932512
var currentHeadersEndOffset = headerBlockEnd;
24942513
var currentBeginOffset = headerBlockBegin;

MimeKit/ParserOptions.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -310,9 +310,9 @@ static bool EqualsAny (string value, params string[] values)
310310
return false;
311311
}
312312

313-
internal MimeEntity CreateEntity (ContentType contentType, IList<Header> headers, bool toplevel, int depth)
313+
internal MimeEntity CreateEntity (ContentType contentType, IList<Header> headers, bool hasBodySeparator, bool toplevel, int depth)
314314
{
315-
var args = new MimeEntityConstructorArgs (this, contentType, headers, toplevel);
315+
var args = new MimeEntityConstructorArgs (this, contentType, headers, hasBodySeparator, toplevel);
316316
var subtype = contentType.MediaSubtype;
317317
var type = contentType.MediaType;
318318

0 commit comments

Comments
 (0)