Skip to content

Commit

Permalink
Updated MimeReader to report non-compliance issues that it discovers
Browse files Browse the repository at this point in the history
  • Loading branch information
jstedfast committed Dec 23, 2024
1 parent 69752cd commit d5accf9
Show file tree
Hide file tree
Showing 4 changed files with 1,020 additions and 29 deletions.
48 changes: 37 additions & 11 deletions MimeKit/AsyncMimeReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,12 @@ async Task StepHeadersAsync (CancellationToken cancellationToken)
boundary = BoundaryType.None;
headerCount = 0;

currentContentEncodingLineNumber = -1;
currentContentEncodingOffset = -1;
currentContentLength = null;
currentContentType = null;
currentEncoding = null;
hasMimeVersion = false;

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

Expand Down Expand Up @@ -166,6 +169,7 @@ async Task StepHeadersAsync (CancellationToken cancellationToken)
}

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

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

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

// Note: If a boundary was discovered, then the state will be updated to MimeParserState.Boundary.
if (state == MimeParserState.Boundary)
if (state == MimeParserState.Boundary) {
OnComplianceIssueEncountered (MimeComplianceStatus.MissingBodySeparator, beginOffset, beginLineNumber);
break;
}

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

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

if (await ReadAheadAsync (1, 0, cancellationToken).ConfigureAwait (false) == 0) {
if (midline)
OnComplianceIssueEncountered (MimeComplianceStatus.IncompleteHeader, beginOffset, beginLineNumber);
else
OnComplianceIssueEncountered (MimeComplianceStatus.MissingBodySeparator, beginOffset, beginLineNumber);
state = MimeParserState.Content;
eof = true;
break;
Expand All @@ -280,11 +293,17 @@ async Task StepHeadersAsync (CancellationToken cancellationToken)
return;
}

var header = CreateHeader (beginOffset, fieldNameLength, headerFieldLength, invalid);
var header = CreateHeader (beginOffset, beginLineNumber, fieldNameLength, headerFieldLength, invalid);

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

#if DEBUG_ME_HARDER
// If the state hasn't been updated at this point, that means there's a bug somewhere in the above loop.
if (state == MimeParserState.MessageHeaders || state == MimeParserState.Headers)
Debugger.Break ();
#endif

headerBlockEnd = GetOffset (inputIndex);

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

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

if (currentContentType != null && !hasMimeVersion)
OnComplianceIssueEncountered (MimeComplianceStatus.MissingMimeVersion, currentBeginOffset, beginLineNumber);

var type = GetContentType (null);
MimeEntityType entityType;
int lines;
Expand Down Expand Up @@ -582,11 +604,10 @@ async Task<int> ConstructMultipartAsync (ContentType contentType, int depth, Can
var beginLineNumber = lineNumber;
long endOffset;

if (marker is null) {
#if DEBUG
Debug.WriteLine ("Multipart without a boundary encountered!");
#endif
if (currentEncoding.HasValue && currentEncoding != ContentEncoding.SevenBit && currentEncoding != ContentEncoding.EightBit)
OnComplianceIssueEncountered (MimeComplianceStatus.InvalidContentTransferEncoding, currentContentEncodingOffset, currentContentEncodingLineNumber);

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

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

if (boundary == BoundaryType.ImmediateEndBoundary) {
// consume the end boundary and read the epilogue (if there is one)
// FIXME: multipart.WriteEndBoundary = true;

var boundaryOffset = GetOffset (inputIndex);
var boundaryLineNumber = lineNumber;

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

// FIXME: multipart.WriteEndBoundary = false;

// We either found the end of the stream or we found a parent's boundary
PopBoundary ();

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

endOffset = GetEndOffset (inputIndex);

OnComplianceIssueEncountered (MimeComplianceStatus.MissingMultipartBoundary, endOffset, lineNumber);

return GetLineCount (beginLineNumber, beginOffset, endOffset);
}

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

Expand Down Expand Up @@ -689,6 +709,7 @@ public async Task ReadEntityAsync (CancellationToken cancellationToken = default
{
var beginLineNumber = lineNumber;

ComplianceStatus = MimeComplianceStatus.Compliant;
state = MimeParserState.Headers;
toplevel = true;

Expand Down Expand Up @@ -757,6 +778,8 @@ public async Task ReadEntityAsync (CancellationToken cancellationToken = default
/// </exception>
public async Task ReadMessageAsync (CancellationToken cancellationToken = default)
{
ComplianceStatus = MimeComplianceStatus.Compliant;

// scan the from-line if we are parsing an mbox
while (state != MimeParserState.MessageHeaders) {
switch (await StepAsync (cancellationToken).ConfigureAwait (false)) {
Expand Down Expand Up @@ -788,6 +811,9 @@ public async Task ReadMessageAsync (CancellationToken cancellationToken = defaul
else
contentEnd = 0;

if (currentContentType != null && !hasMimeVersion)
OnComplianceIssueEncountered (MimeComplianceStatus.MissingMimeVersion, currentBeginOffset, beginLineNumber);

var type = GetContentType (null);
MimeEntityType entityType;
int lines;
Expand Down
126 changes: 126 additions & 0 deletions MimeKit/MimeComplianceStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//
// MimeComplianceStatus.cs
//
// Author: Jeffrey Stedfast <[email protected]>
//
// Copyright (c) 2013-2024 .NET Foundation and Contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

using System;

namespace MimeKit
{
/// <summary>
/// A bitfield of potential MIME compliance issues.
/// </summary>
/// <remarks>
/// A bitfield of potential MIME compliance issues.
/// </remarks>
[Flags]
public enum MimeComplianceStatus {
/// <summary>
/// The MIME is compliant.
/// </summary>
Compliant = 0,

/// <summary>
/// The header was not of the correct form.
/// </summary>
InvalidHeader = 1 << 0,

/// <summary>
/// The header ended prematurely at the end of the stream.
/// </summary>
IncompleteHeader = 1 << 1,

/// <summary>
/// The Content-Transfer-Encoding header value was not valid.
/// </summary>
InvalidContentTransferEncoding = 1 << 2,

/// <summary>
/// The Content-Type header value was not valid.
/// </summary>
InvalidContentType = 1 << 3,

/// <summary>
/// The MIME-Version header value was not valid.
/// </summary>
InvalidMimeVersion = 1 << 4,

/// <summary>
/// A line was found that was longer than the SMTP limit of 1000 characters.
/// </summary>
InvalidWrapping = 1 << 5,

/// <summary>
/// An empty line separating the headers from the body was missing.
/// </summary>
MissingBodySeparator = 1 << 6,

/// <summary>
/// The MIME-Version header is missing.
/// </summary>
MissingMimeVersion = 1 << 7,

/// <summary>
/// The boundary parameter is missing from a multipart Content-Type header.
/// </summary>
MissingMultipartBoundaryParameter = 1 << 8,

/// <summary>
/// A multipart boundary was missing.
/// </summary>
MissingMultipartBoundary = 1 << 9,

/// <summary>
/// A MIME part contained multiple Content-Transfer-Encoding headers.
/// </summary>
DuplicateContentTransferEncoding = 1 << 10,

/// <summary>
/// A MIME part contained multiple Content-Type headers.
/// </summary>
DuplicateContentType = 1 << 11,

#if false
/// <summary>
/// A line was found in a MIME part body content that was linefeed terminated instead of carriage return &amp; linefeed terminated.
/// </summary>
BareLinefeedInBody,

/// <summary>
/// An external body was specified with invalid syntax.
/// </summary>
InvalidExternalBody,

/// <summary>
/// A line was found in a MIME part header that was linefeed terminated instead of carriage return &amp; linefeed terminated.
/// </summary>
BareLinefeedInHeader,

/// <summary>
/// Unexpected binary content was found in MIME part body content.
/// </summary>
UnexpectedBinaryContent,
#endif
}
}
Loading

0 comments on commit d5accf9

Please sign in to comment.