Skip to content

Commit d5accf9

Browse files
committed
Updated MimeReader to report non-compliance issues that it discovers
1 parent 69752cd commit d5accf9

File tree

4 files changed

+1020
-29
lines changed

4 files changed

+1020
-29
lines changed

MimeKit/AsyncMimeReader.cs

+37-11
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,12 @@ async Task StepHeadersAsync (CancellationToken cancellationToken)
135135
boundary = BoundaryType.None;
136136
headerCount = 0;
137137

138+
currentContentEncodingLineNumber = -1;
139+
currentContentEncodingOffset = -1;
138140
currentContentLength = null;
139141
currentContentType = null;
140142
currentEncoding = null;
143+
hasMimeVersion = false;
141144

142145
await OnHeadersBeginAsync (headerBlockBegin, headersBeginLineNumber, cancellationToken).ConfigureAwait (false);
143146

@@ -166,6 +169,7 @@ async Task StepHeadersAsync (CancellationToken cancellationToken)
166169
}
167170

168171
// Note: This can happen if a message is truncated immediately after a boundary marker (e.g. where subpart headers would begin).
172+
OnComplianceIssueEncountered (MimeComplianceStatus.MissingBodySeparator, beginOffset, beginLineNumber);
169173
state = MimeParserState.Content;
170174
break;
171175
}
@@ -196,6 +200,7 @@ async Task StepHeadersAsync (CancellationToken cancellationToken)
196200

197201
if (invalid) {
198202
// Figure out why this is an invalid header.
203+
OnComplianceIssueEncountered (MimeComplianceStatus.InvalidHeader, beginOffset, lineNumber);
199204

200205
if (input[inputIndex] == (byte) '-') {
201206
// Check for a boundary marker. If the message is properly formatted, this will NEVER happen.
@@ -214,8 +219,10 @@ async Task StepHeadersAsync (CancellationToken cancellationToken)
214219
} while (true);
215220

216221
// Note: If a boundary was discovered, then the state will be updated to MimeParserState.Boundary.
217-
if (state == MimeParserState.Boundary)
222+
if (state == MimeParserState.Boundary) {
223+
OnComplianceIssueEncountered (MimeComplianceStatus.MissingBodySeparator, beginOffset, beginLineNumber);
218224
break;
225+
}
219226

220227
// Fall through and act as if we're consuming a header.
221228
} else if (input[inputIndex] == (byte) 'F' || input[inputIndex] == (byte) '>') {
@@ -238,8 +245,10 @@ async Task StepHeadersAsync (CancellationToken cancellationToken)
238245
// 1. Complete: This means that we've found an actual mbox marker
239246
// 2. Error: Invalid *first* header and it was not a valid mbox marker
240247
// 3. MessageHeaders or Headers: let it fall through and treat it as an invalid headers
241-
if (state != MimeParserState.MessageHeaders && state != MimeParserState.Headers)
248+
if (state != MimeParserState.MessageHeaders && state != MimeParserState.Headers) {
249+
OnComplianceIssueEncountered (MimeComplianceStatus.MissingBodySeparator, beginOffset, beginLineNumber);
242250
break;
251+
}
243252

244253
// Fall through and act as if we're consuming a header.
245254
} else {
@@ -269,6 +278,10 @@ async Task StepHeadersAsync (CancellationToken cancellationToken)
269278
}
270279

271280
if (await ReadAheadAsync (1, 0, cancellationToken).ConfigureAwait (false) == 0) {
281+
if (midline)
282+
OnComplianceIssueEncountered (MimeComplianceStatus.IncompleteHeader, beginOffset, beginLineNumber);
283+
else
284+
OnComplianceIssueEncountered (MimeComplianceStatus.MissingBodySeparator, beginOffset, beginLineNumber);
272285
state = MimeParserState.Content;
273286
eof = true;
274287
break;
@@ -280,11 +293,17 @@ async Task StepHeadersAsync (CancellationToken cancellationToken)
280293
return;
281294
}
282295

283-
var header = CreateHeader (beginOffset, fieldNameLength, headerFieldLength, invalid);
296+
var header = CreateHeader (beginOffset, beginLineNumber, fieldNameLength, headerFieldLength, invalid);
284297

285298
await OnHeaderReadAsync (header, beginLineNumber, cancellationToken).ConfigureAwait (false);
286299
} while (!eof);
287300

301+
#if DEBUG_ME_HARDER
302+
// If the state hasn't been updated at this point, that means there's a bug somewhere in the above loop.
303+
if (state == MimeParserState.MessageHeaders || state == MimeParserState.Headers)
304+
Debugger.Break ();
305+
#endif
306+
288307
headerBlockEnd = GetOffset (inputIndex);
289308

290309
await OnHeadersEndAsync (headerBlockBegin, headersBeginLineNumber, headerBlockEnd, lineNumber, cancellationToken).ConfigureAwait (false);
@@ -442,6 +461,9 @@ async Task<int> ConstructMessagePartAsync (int depth, CancellationToken cancella
442461

443462
await OnMimeMessageBeginAsync (currentBeginOffset, beginLineNumber, cancellationToken).ConfigureAwait (false);
444463

464+
if (currentContentType != null && !hasMimeVersion)
465+
OnComplianceIssueEncountered (MimeComplianceStatus.MissingMimeVersion, currentBeginOffset, beginLineNumber);
466+
445467
var type = GetContentType (null);
446468
MimeEntityType entityType;
447469
int lines;
@@ -582,11 +604,10 @@ async Task<int> ConstructMultipartAsync (ContentType contentType, int depth, Can
582604
var beginLineNumber = lineNumber;
583605
long endOffset;
584606

585-
if (marker is null) {
586-
#if DEBUG
587-
Debug.WriteLine ("Multipart without a boundary encountered!");
588-
#endif
607+
if (currentEncoding.HasValue && currentEncoding != ContentEncoding.SevenBit && currentEncoding != ContentEncoding.EightBit)
608+
OnComplianceIssueEncountered (MimeComplianceStatus.InvalidContentTransferEncoding, currentContentEncodingOffset, currentContentEncodingLineNumber);
589609

610+
if (marker is null) {
590611
// Note: this will scan all content into the preamble...
591612
await MultipartScanPreambleAsync (cancellationToken).ConfigureAwait (false);
592613

@@ -603,8 +624,6 @@ async Task<int> ConstructMultipartAsync (ContentType contentType, int depth, Can
603624

604625
if (boundary == BoundaryType.ImmediateEndBoundary) {
605626
// consume the end boundary and read the epilogue (if there is one)
606-
// FIXME: multipart.WriteEndBoundary = true;
607-
608627
var boundaryOffset = GetOffset (inputIndex);
609628
var boundaryLineNumber = lineNumber;
610629

@@ -621,8 +640,6 @@ async Task<int> ConstructMultipartAsync (ContentType contentType, int depth, Can
621640
return GetLineCount (beginLineNumber, beginOffset, endOffset);
622641
}
623642

624-
// FIXME: multipart.WriteEndBoundary = false;
625-
626643
// We either found the end of the stream or we found a parent's boundary
627644
PopBoundary ();
628645

@@ -637,6 +654,8 @@ async Task<int> ConstructMultipartAsync (ContentType contentType, int depth, Can
637654

638655
endOffset = GetEndOffset (inputIndex);
639656

657+
OnComplianceIssueEncountered (MimeComplianceStatus.MissingMultipartBoundary, endOffset, lineNumber);
658+
640659
return GetLineCount (beginLineNumber, beginOffset, endOffset);
641660
}
642661

@@ -659,6 +678,7 @@ async Task<int> ConstructMultipartAsync (ContentType contentType, int depth, Can
659678
/// </exception>
660679
public async Task ReadHeadersAsync (CancellationToken cancellationToken = default)
661680
{
681+
ComplianceStatus = MimeComplianceStatus.Compliant;
662682
state = MimeParserState.Headers;
663683
toplevel = true;
664684

@@ -689,6 +709,7 @@ public async Task ReadEntityAsync (CancellationToken cancellationToken = default
689709
{
690710
var beginLineNumber = lineNumber;
691711

712+
ComplianceStatus = MimeComplianceStatus.Compliant;
692713
state = MimeParserState.Headers;
693714
toplevel = true;
694715

@@ -757,6 +778,8 @@ public async Task ReadEntityAsync (CancellationToken cancellationToken = default
757778
/// </exception>
758779
public async Task ReadMessageAsync (CancellationToken cancellationToken = default)
759780
{
781+
ComplianceStatus = MimeComplianceStatus.Compliant;
782+
760783
// scan the from-line if we are parsing an mbox
761784
while (state != MimeParserState.MessageHeaders) {
762785
switch (await StepAsync (cancellationToken).ConfigureAwait (false)) {
@@ -788,6 +811,9 @@ public async Task ReadMessageAsync (CancellationToken cancellationToken = defaul
788811
else
789812
contentEnd = 0;
790813

814+
if (currentContentType != null && !hasMimeVersion)
815+
OnComplianceIssueEncountered (MimeComplianceStatus.MissingMimeVersion, currentBeginOffset, beginLineNumber);
816+
791817
var type = GetContentType (null);
792818
MimeEntityType entityType;
793819
int lines;

MimeKit/MimeComplianceStatus.cs

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
//
2+
// MimeComplianceStatus.cs
3+
//
4+
// Author: Jeffrey Stedfast <[email protected]>
5+
//
6+
// Copyright (c) 2013-2024 .NET Foundation and Contributors
7+
//
8+
// Permission is hereby granted, free of charge, to any person obtaining a copy
9+
// of this software and associated documentation files (the "Software"), to deal
10+
// in the Software without restriction, including without limitation the rights
11+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
// copies of the Software, and to permit persons to whom the Software is
13+
// furnished to do so, subject to the following conditions:
14+
//
15+
// The above copyright notice and this permission notice shall be included in
16+
// all copies or substantial portions of the Software.
17+
//
18+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
// THE SOFTWARE.
25+
//
26+
27+
using System;
28+
29+
namespace MimeKit
30+
{
31+
/// <summary>
32+
/// A bitfield of potential MIME compliance issues.
33+
/// </summary>
34+
/// <remarks>
35+
/// A bitfield of potential MIME compliance issues.
36+
/// </remarks>
37+
[Flags]
38+
public enum MimeComplianceStatus {
39+
/// <summary>
40+
/// The MIME is compliant.
41+
/// </summary>
42+
Compliant = 0,
43+
44+
/// <summary>
45+
/// The header was not of the correct form.
46+
/// </summary>
47+
InvalidHeader = 1 << 0,
48+
49+
/// <summary>
50+
/// The header ended prematurely at the end of the stream.
51+
/// </summary>
52+
IncompleteHeader = 1 << 1,
53+
54+
/// <summary>
55+
/// The Content-Transfer-Encoding header value was not valid.
56+
/// </summary>
57+
InvalidContentTransferEncoding = 1 << 2,
58+
59+
/// <summary>
60+
/// The Content-Type header value was not valid.
61+
/// </summary>
62+
InvalidContentType = 1 << 3,
63+
64+
/// <summary>
65+
/// The MIME-Version header value was not valid.
66+
/// </summary>
67+
InvalidMimeVersion = 1 << 4,
68+
69+
/// <summary>
70+
/// A line was found that was longer than the SMTP limit of 1000 characters.
71+
/// </summary>
72+
InvalidWrapping = 1 << 5,
73+
74+
/// <summary>
75+
/// An empty line separating the headers from the body was missing.
76+
/// </summary>
77+
MissingBodySeparator = 1 << 6,
78+
79+
/// <summary>
80+
/// The MIME-Version header is missing.
81+
/// </summary>
82+
MissingMimeVersion = 1 << 7,
83+
84+
/// <summary>
85+
/// The boundary parameter is missing from a multipart Content-Type header.
86+
/// </summary>
87+
MissingMultipartBoundaryParameter = 1 << 8,
88+
89+
/// <summary>
90+
/// A multipart boundary was missing.
91+
/// </summary>
92+
MissingMultipartBoundary = 1 << 9,
93+
94+
/// <summary>
95+
/// A MIME part contained multiple Content-Transfer-Encoding headers.
96+
/// </summary>
97+
DuplicateContentTransferEncoding = 1 << 10,
98+
99+
/// <summary>
100+
/// A MIME part contained multiple Content-Type headers.
101+
/// </summary>
102+
DuplicateContentType = 1 << 11,
103+
104+
#if false
105+
/// <summary>
106+
/// A line was found in a MIME part body content that was linefeed terminated instead of carriage return &amp; linefeed terminated.
107+
/// </summary>
108+
BareLinefeedInBody,
109+
110+
/// <summary>
111+
/// An external body was specified with invalid syntax.
112+
/// </summary>
113+
InvalidExternalBody,
114+
115+
/// <summary>
116+
/// A line was found in a MIME part header that was linefeed terminated instead of carriage return &amp; linefeed terminated.
117+
/// </summary>
118+
BareLinefeedInHeader,
119+
120+
/// <summary>
121+
/// Unexpected binary content was found in MIME part body content.
122+
/// </summary>
123+
UnexpectedBinaryContent,
124+
#endif
125+
}
126+
}

0 commit comments

Comments
 (0)