diff --git a/MimeKit/AsyncMimeReader.cs b/MimeKit/AsyncMimeReader.cs index dc86844409..3ce67786ad 100644 --- a/MimeKit/AsyncMimeReader.cs +++ b/MimeKit/AsyncMimeReader.cs @@ -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); @@ -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; } @@ -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. @@ -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) '>') { @@ -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 { @@ -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; @@ -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); @@ -442,6 +461,9 @@ async Task 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; @@ -582,11 +604,10 @@ async Task 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); @@ -603,8 +624,6 @@ async Task 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; @@ -621,8 +640,6 @@ async Task 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 (); @@ -637,6 +654,8 @@ async Task ConstructMultipartAsync (ContentType contentType, int depth, Can endOffset = GetEndOffset (inputIndex); + OnComplianceIssueEncountered (MimeComplianceStatus.MissingMultipartBoundary, endOffset, lineNumber); + return GetLineCount (beginLineNumber, beginOffset, endOffset); } @@ -659,6 +678,7 @@ async Task ConstructMultipartAsync (ContentType contentType, int depth, Can /// public async Task ReadHeadersAsync (CancellationToken cancellationToken = default) { + ComplianceStatus = MimeComplianceStatus.Compliant; state = MimeParserState.Headers; toplevel = true; @@ -689,6 +709,7 @@ public async Task ReadEntityAsync (CancellationToken cancellationToken = default { var beginLineNumber = lineNumber; + ComplianceStatus = MimeComplianceStatus.Compliant; state = MimeParserState.Headers; toplevel = true; @@ -757,6 +778,8 @@ public async Task ReadEntityAsync (CancellationToken cancellationToken = default /// 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)) { @@ -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; diff --git a/MimeKit/MimeComplianceStatus.cs b/MimeKit/MimeComplianceStatus.cs new file mode 100644 index 0000000000..d279778a24 --- /dev/null +++ b/MimeKit/MimeComplianceStatus.cs @@ -0,0 +1,126 @@ +// +// MimeComplianceStatus.cs +// +// Author: Jeffrey Stedfast +// +// 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 +{ + /// + /// A bitfield of potential MIME compliance issues. + /// + /// + /// A bitfield of potential MIME compliance issues. + /// + [Flags] + public enum MimeComplianceStatus { + /// + /// The MIME is compliant. + /// + Compliant = 0, + + /// + /// The header was not of the correct form. + /// + InvalidHeader = 1 << 0, + + /// + /// The header ended prematurely at the end of the stream. + /// + IncompleteHeader = 1 << 1, + + /// + /// The Content-Transfer-Encoding header value was not valid. + /// + InvalidContentTransferEncoding = 1 << 2, + + /// + /// The Content-Type header value was not valid. + /// + InvalidContentType = 1 << 3, + + /// + /// The MIME-Version header value was not valid. + /// + InvalidMimeVersion = 1 << 4, + + /// + /// A line was found that was longer than the SMTP limit of 1000 characters. + /// + InvalidWrapping = 1 << 5, + + /// + /// An empty line separating the headers from the body was missing. + /// + MissingBodySeparator = 1 << 6, + + /// + /// The MIME-Version header is missing. + /// + MissingMimeVersion = 1 << 7, + + /// + /// The boundary parameter is missing from a multipart Content-Type header. + /// + MissingMultipartBoundaryParameter = 1 << 8, + + /// + /// A multipart boundary was missing. + /// + MissingMultipartBoundary = 1 << 9, + + /// + /// A MIME part contained multiple Content-Transfer-Encoding headers. + /// + DuplicateContentTransferEncoding = 1 << 10, + + /// + /// A MIME part contained multiple Content-Type headers. + /// + DuplicateContentType = 1 << 11, + +#if false + /// + /// A line was found in a MIME part body content that was linefeed terminated instead of carriage return & linefeed terminated. + /// + BareLinefeedInBody, + + /// + /// An external body was specified with invalid syntax. + /// + InvalidExternalBody, + + /// + /// A line was found in a MIME part header that was linefeed terminated instead of carriage return & linefeed terminated. + /// + BareLinefeedInHeader, + + /// + /// Unexpected binary content was found in MIME part body content. + /// + UnexpectedBinaryContent, +#endif + } +} diff --git a/MimeKit/MimeReader.cs b/MimeKit/MimeReader.cs index 9fed517c60..733e352481 100644 --- a/MimeKit/MimeReader.cs +++ b/MimeKit/MimeReader.cs @@ -52,6 +52,7 @@ enum MimeEntityType } static ReadOnlySpan UTF8ByteOrderMark => new byte[] { 0xEF, 0xBB, 0xBF }; + static readonly Version MimeVersion = new Version (1, 0); const int HeaderBufferGrowSize = 64; const int ReadAheadSize = 128; @@ -71,9 +72,12 @@ enum MimeEntityType readonly List bounds = new List (); + int currentContentEncodingLineNumber; + long currentContentEncodingOffset; ContentEncoding? currentEncoding; ContentType currentContentType; long? currentContentLength; + bool hasMimeVersion; MimeParserState state; BoundaryType boundary; @@ -177,6 +181,18 @@ public long Position { get { return GetOffset (inputIndex); } } + /// + /// Get the current compliance status of the MIME stream. + /// + /// + /// Gets the current compliance status of the MIME stream. + /// As the reader progresses, this value may change if compliance issues are encountered. + /// + /// The compliance status. + public MimeComplianceStatus ComplianceStatus { + get; protected set; + } + /// /// Set the stream to parse. /// @@ -193,6 +209,8 @@ public virtual void SetStream (Stream stream, MimeFormat format = MimeFormat.Def if (stream is null) throw new ArgumentNullException (nameof (stream)); + ComplianceStatus = MimeComplianceStatus.Compliant; + this.format = format; this.stream = stream; @@ -218,6 +236,24 @@ public virtual void SetStream (Stream stream, MimeFormat format = MimeFormat.Def boundary = BoundaryType.None; } + #region ComplianceStatus Events + + /// + /// Called when a MIME compliance issue is encountered in the stream. + /// + /// + /// When a MIME compliance issue is encountered while reading, this method will be called with information about the issue discovered. + /// + /// The MIME compliance issue that was encountered. + /// The offset into the stream where the MIME compliance issue was encountered. + /// The line number where the MIME compliance issue was encountered. + protected virtual void OnComplianceIssueEncountered (MimeComplianceStatus issue, long offset, int lineNumber) + { + ComplianceStatus |= issue; + } + + #endregion ComplianceStatus Events + #region Mbox Events /// @@ -1159,6 +1195,9 @@ void IncrementLineNumber (int index) prevLineBeginOffset = lineBeginOffset; lineBeginOffset = GetOffset (index); lineNumber++; + + if ((lineBeginOffset - prevLineBeginOffset) >= 1000) + OnComplianceIssueEncountered (MimeComplianceStatus.InvalidWrapping, prevLineBeginOffset, lineNumber - 1); } static unsafe bool CStringsEqual (byte* str1, byte* str2, int length) @@ -1367,7 +1406,7 @@ unsafe void StepMboxMarker (byte* inbuf, CancellationToken cancellationToken) state = MimeParserState.MessageHeaders; } - void UpdateHeaderState (Header header) + void UpdateHeaderState (Header header, int beginLineNumber) { var rawValue = header.RawValue; int index = 0; @@ -1375,8 +1414,14 @@ void UpdateHeaderState (Header header) switch (header.Id) { case HeaderId.ContentTransferEncoding: if (!currentEncoding.HasValue) { - MimeUtils.TryParse (header.Value, out ContentEncoding encoding); + if (!MimeUtils.TryParse (header.Value, out ContentEncoding encoding)) + OnComplianceIssueEncountered (MimeComplianceStatus.InvalidContentTransferEncoding, header.Offset.Value, beginLineNumber); + + currentContentEncodingLineNumber = beginLineNumber; + currentContentEncodingOffset = header.Offset.Value; currentEncoding = encoding; + } else { + OnComplianceIssueEncountered (MimeComplianceStatus.DuplicateContentTransferEncoding, header.Offset.Value, beginLineNumber); } break; case HeaderId.ContentLength: @@ -1387,8 +1432,9 @@ void UpdateHeaderState (Header header) break; case HeaderId.ContentType: if (currentContentType is null) { - // FIXME: do we really need all this fallback stuff for parameters? I doubt it. if (!ContentType.TryParse (options, rawValue, ref index, rawValue.Length, false, out var type) && type is null) { + OnComplianceIssueEncountered (MimeComplianceStatus.InvalidContentType, header.Offset.Value, beginLineNumber); + // if 'type' is null, then it means that even the mime-type was unintelligible type = new ContentType ("application", "octet-stream"); @@ -1402,9 +1448,19 @@ void UpdateHeaderState (Header header) } } + if (IsMultipart (type) && string.IsNullOrEmpty (type.Boundary)) + OnComplianceIssueEncountered (MimeComplianceStatus.MissingMultipartBoundaryParameter, header.Offset.Value, beginLineNumber); + currentContentType = type; + } else { + OnComplianceIssueEncountered (MimeComplianceStatus.DuplicateContentType, header.Offset.Value, beginLineNumber); } break; + case HeaderId.MimeVersion: + if (!MimeUtils.TryParse (header.Value, out Version version) || !version.Equals (MimeVersion)) + OnComplianceIssueEncountered (MimeComplianceStatus.InvalidMimeVersion, header.Offset.Value, beginLineNumber); + hasMimeVersion = true; + break; } } @@ -1639,7 +1695,7 @@ unsafe bool TryCheckMboxMarkerWithinHeaderBlock (byte* inbuf) return true; } - Header CreateHeader (long beginOffset, int fieldNameLength, int headerFieldLength, bool invalid) + Header CreateHeader (long beginOffset, int beginLineNumber, int fieldNameLength, int headerFieldLength, bool invalid) { byte[] field, value; @@ -1659,7 +1715,7 @@ Header CreateHeader (long beginOffset, int fieldNameLength, int headerFieldLengt Offset = beginOffset }; - UpdateHeaderState (header); + UpdateHeaderState (header, beginLineNumber); headerCount++; return header; @@ -1674,9 +1730,12 @@ unsafe void StepHeaders (byte* inbuf, CancellationToken cancellationToken) boundary = BoundaryType.None; headerCount = 0; + currentContentEncodingLineNumber = -1; + currentContentEncodingOffset = -1; currentContentLength = null; currentContentType = null; currentEncoding = null; + hasMimeVersion = false; OnHeadersBegin (headerBlockBegin, headersBeginLineNumber, cancellationToken); @@ -1705,6 +1764,7 @@ unsafe void StepHeaders (byte* inbuf, 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; } @@ -1728,6 +1788,7 @@ unsafe void StepHeaders (byte* inbuf, 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. @@ -1739,8 +1800,10 @@ unsafe void StepHeaders (byte* inbuf, CancellationToken cancellationToken) } // 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) '>') { @@ -1756,8 +1819,10 @@ unsafe void StepHeaders (byte* inbuf, 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 { @@ -1780,6 +1845,10 @@ unsafe void StepHeaders (byte* inbuf, CancellationToken cancellationToken) // Consume the header value. while (!StepHeaderValue (inbuf, ref midline)) { if (ReadAhead (1, 0, cancellationToken) == 0) { + if (midline) + OnComplianceIssueEncountered (MimeComplianceStatus.IncompleteHeader, beginOffset, beginLineNumber); + else + OnComplianceIssueEncountered (MimeComplianceStatus.MissingBodySeparator, beginOffset, beginLineNumber); state = MimeParserState.Content; eof = true; break; @@ -1791,11 +1860,17 @@ unsafe void StepHeaders (byte* inbuf, CancellationToken cancellationToken) return; } - var header = CreateHeader (beginOffset, fieldNameLength, headerFieldLength, invalid); + var header = CreateHeader (beginOffset, beginLineNumber, fieldNameLength, headerFieldLength, invalid); OnHeaderRead (header, beginLineNumber, cancellationToken); } 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); OnHeadersEnd (headerBlockBegin, headersBeginLineNumber, headerBlockEnd, lineNumber, cancellationToken); @@ -2202,6 +2277,9 @@ unsafe int ConstructMessagePart (byte* inbuf, int depth, CancellationToken cance OnMimeMessageBegin (currentBeginOffset, beginLineNumber, cancellationToken); + if (currentContentType != null && !hasMimeVersion) + OnComplianceIssueEncountered (MimeComplianceStatus.MissingMimeVersion, currentBeginOffset, beginLineNumber); + var type = GetContentType (null); MimeEntityType entityType; int lines; @@ -2355,11 +2433,10 @@ unsafe int ConstructMultipart (ContentType contentType, byte* inbuf, int depth, 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... MultipartScanPreamble (inbuf, cancellationToken); @@ -2402,11 +2479,14 @@ unsafe int ConstructMultipart (ContentType contentType, byte* inbuf, int depth, endOffset = GetEndOffset (inputIndex); + OnComplianceIssueEncountered (MimeComplianceStatus.MissingMultipartBoundary, endOffset, lineNumber); + return GetLineCount (beginLineNumber, beginOffset, endOffset); } unsafe void ReadHeaders (byte* inbuf, CancellationToken cancellationToken) { + ComplianceStatus = MimeComplianceStatus.Compliant; state = MimeParserState.Headers; toplevel = true; @@ -2445,6 +2525,7 @@ unsafe void ReadEntity (byte* inbuf, CancellationToken cancellationToken) { var beginLineNumber = lineNumber; + ComplianceStatus = MimeComplianceStatus.Compliant; state = MimeParserState.Headers; toplevel = true; @@ -2521,6 +2602,8 @@ public void ReadEntity (CancellationToken cancellationToken = default) unsafe void ReadMessage (byte* inbuf, CancellationToken cancellationToken) { + ComplianceStatus = MimeComplianceStatus.Compliant; + // scan the from-line if we are parsing an mbox while (state != MimeParserState.MessageHeaders) { switch (Step (inbuf, cancellationToken)) { @@ -2552,6 +2635,9 @@ unsafe void ReadMessage (byte* inbuf, CancellationToken cancellationToken) else contentEnd = 0; + if (currentContentType != null && !hasMimeVersion) + OnComplianceIssueEncountered (MimeComplianceStatus.MissingMimeVersion, currentBeginOffset, beginLineNumber); + var type = GetContentType (null); MimeEntityType entityType; int lines; diff --git a/UnitTests/MimeReaderTests.cs b/UnitTests/MimeReaderTests.cs index 44f0d89b37..5c74a0c3fa 100644 --- a/UnitTests/MimeReaderTests.cs +++ b/UnitTests/MimeReaderTests.cs @@ -417,6 +417,7 @@ public void TestLineCountSingleLine () var lines = reader.Offsets[0].Body.Lines; Assert.That (lines, Is.EqualTo (1), "Line count"); + Assert.That (reader.ComplianceStatus, Is.EqualTo (MimeComplianceStatus.Compliant), "ComplianceStatus"); } } @@ -440,6 +441,7 @@ public async Task TestLineCountSingleLineAsync () var lines = reader.Offsets[0].Body.Lines; Assert.That (lines, Is.EqualTo (1), "Line count"); + Assert.That (reader.ComplianceStatus, Is.EqualTo (MimeComplianceStatus.Compliant), "ComplianceStatus"); } } @@ -464,6 +466,7 @@ This is a single line of text var lines = reader.Offsets[0].Body.Lines; Assert.That (lines, Is.EqualTo (1), "Line count"); + Assert.That (reader.ComplianceStatus, Is.EqualTo (MimeComplianceStatus.Compliant), "ComplianceStatus"); } } @@ -488,6 +491,7 @@ This is a single line of text var lines = reader.Offsets[0].Body.Lines; Assert.That (lines, Is.EqualTo (1), "Line count"); + Assert.That (reader.ComplianceStatus, Is.EqualTo (MimeComplianceStatus.Compliant), "ComplianceStatus"); } } @@ -521,6 +525,7 @@ This is a single line of text var lines = reader.Offsets[0].Body.Children[0].Lines; Assert.That (lines, Is.EqualTo (1), "Line count"); + Assert.That (reader.ComplianceStatus, Is.EqualTo (MimeComplianceStatus.Compliant), "ComplianceStatus"); } } @@ -554,6 +559,7 @@ This is a single line of text var lines = reader.Offsets[0].Body.Children[0].Lines; Assert.That (lines, Is.EqualTo (1), "Line count"); + Assert.That (reader.ComplianceStatus, Is.EqualTo (MimeComplianceStatus.Compliant), "ComplianceStatus"); } } @@ -581,13 +587,14 @@ This is a single line of text followed by a blank line "; using (var stream = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { - var parser = new CustomMimeReader (stream, MimeFormat.Entity); + var reader = new CustomMimeReader (stream, MimeFormat.Entity); - parser.ReadMessage (); + reader.ReadMessage (); - var lines = parser.Offsets[0].Body.Children[0].Lines; + var lines = reader.Offsets[0].Body.Children[0].Lines; Assert.That (lines, Is.EqualTo (1), "Line count"); + Assert.That (reader.ComplianceStatus, Is.EqualTo (MimeComplianceStatus.Compliant), "ComplianceStatus"); } } @@ -615,13 +622,14 @@ This is a single line of text followed by a blank line "; using (var stream = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { - var parser = new CustomMimeReader (stream, MimeFormat.Entity); + var reader = new CustomMimeReader (stream, MimeFormat.Entity); - await parser.ReadMessageAsync (); + await reader.ReadMessageAsync (); - var lines = parser.Offsets[0].Body.Children[0].Lines; + var lines = reader.Offsets[0].Body.Children[0].Lines; Assert.That (lines, Is.EqualTo (1), "Line count"); + Assert.That (reader.ComplianceStatus, Is.EqualTo (MimeComplianceStatus.Compliant), "ComplianceStatus"); } } @@ -638,6 +646,7 @@ public void TestLineCountNonTerminatedSingleHeader () var lines = reader.Offsets[0].Body.Lines; Assert.That (lines, Is.EqualTo (0), "Line count"); + Assert.That (reader.ComplianceStatus, Is.EqualTo (MimeComplianceStatus.IncompleteHeader), "ComplianceStatus"); } } @@ -654,6 +663,7 @@ public async Task TestLineCountNonTerminatedSingleHeaderAsync () var lines = reader.Offsets[0].Body.Lines; Assert.That (lines, Is.EqualTo (0), "Line count"); + Assert.That (reader.ComplianceStatus, Is.EqualTo (MimeComplianceStatus.IncompleteHeader), "ComplianceStatus"); } } @@ -670,6 +680,7 @@ public void TestLineCountProperlyTerminatedSingleHeader () var lines = reader.Offsets[0].Body.Lines; Assert.That (lines, Is.EqualTo (0), "Line count"); + Assert.That (reader.ComplianceStatus, Is.EqualTo (MimeComplianceStatus.MissingBodySeparator), "ComplianceStatus"); } } @@ -686,7 +697,749 @@ public async Task TestLineCountProperlyTerminatedSingleHeaderAsync () var lines = reader.Offsets[0].Body.Lines; Assert.That (lines, Is.EqualTo (0), "Line count"); + Assert.That (reader.ComplianceStatus, Is.EqualTo (MimeComplianceStatus.MissingBodySeparator), "ComplianceStatus"); + } + } + + class MimeComplianceIssue + { + public readonly MimeComplianceStatus Issue; + public readonly int LineNumber; + public readonly int Column; + public long UnixOffset; + public long DosOffset; + + public MimeComplianceIssue (MimeComplianceStatus issue, int lineNumber, int column) + { + Issue = issue; + LineNumber = lineNumber; + Column = column; } } + + class MimeComplianceReader : MimeReader + { + readonly MimeComplianceIssue[] issues; + readonly NewLineFormat format; + readonly string text; + int issueIndex; + + public MimeComplianceReader (string text, Stream stream, NewLineFormat format, params MimeComplianceIssue[] issues) : base (stream, MimeFormat.Entity) + { + this.text = text; + this.issues = issues; + this.format = format; + } + + protected override void OnComplianceIssueEncountered (MimeComplianceStatus issue, long offset, int lineNumber) + { + Assert.That (issueIndex, Is.LessThan (issues.Length), $"Too many compliance issues encountered ({format}): {issue}"); + Assert.That (issue, Is.EqualTo (issues[issueIndex].Issue), $"Compliance issue mismatch ({format})."); + if (format == NewLineFormat.Unix) + Assert.That (offset, Is.EqualTo (issues[issueIndex].UnixOffset), $"Stream offset mismatch ({format}). Expected: {text.Substring ((int) issues[issueIndex].UnixOffset)}"); + else + Assert.That (offset, Is.EqualTo (issues[issueIndex].DosOffset), $"Stream offset mismatch ({format})."); + Assert.That (lineNumber, Is.EqualTo (issues[issueIndex].LineNumber), $"Line number mismatch ({format})."); + issueIndex++; + + base.OnComplianceIssueEncountered (issue, offset, lineNumber); + } + } + + static void UpdateStreamOffsets (string text, MimeComplianceIssue[] issues) + { + long unixOffset = 0; + long dosOffset = 0; + int lineNumber = 1; + int column = 1; + + for (int i = 0; i < text.Length; i++) { + if (text[i] == '\n') { + dosOffset += 2; + unixOffset++; + lineNumber++; + column = 1; + } else { + unixOffset++; + dosOffset++; + column++; + } + + foreach (var issue in issues) { + if (issue.LineNumber == lineNumber && issue.Column == column) { + issue.UnixOffset = unixOffset; + issue.DosOffset = dosOffset; + } + } + } + } + + static void AssertMimeComplianceIssues (string text, MimeComplianceIssue[] issues) + { + var unix = text.Replace ("\r\n", "\n"); + var dos = unix.Replace ("\n", "\r\n"); + + UpdateStreamOffsets (unix, issues); + + using (var stream = new MemoryStream (Encoding.ASCII.GetBytes (unix), false)) { + var reader = new MimeComplianceReader (unix, stream, NewLineFormat.Unix, issues); + + reader.ReadMessage (); + } + + using (var stream = new MemoryStream (Encoding.ASCII.GetBytes (dos), false)) { + var reader = new MimeComplianceReader (dos, stream, NewLineFormat.Dos, issues); + + reader.ReadMessage (); + } + } + + static async Task AssertMimeComplianceIssuesAsync (string text, MimeComplianceIssue[] issues) + { + var unix = text.Replace ("\r\n", "\n"); + var dos = unix.Replace ("\n", "\r\n"); + + UpdateStreamOffsets (unix, issues); + + using (var stream = new MemoryStream (Encoding.ASCII.GetBytes (unix), false)) { + var reader = new MimeComplianceReader (unix, stream, NewLineFormat.Unix, issues); + + await reader.ReadMessageAsync (); + } + + using (var stream = new MemoryStream (Encoding.ASCII.GetBytes (dos), false)) { + var reader = new MimeComplianceReader (dos, stream, NewLineFormat.Dos, issues); + + await reader.ReadMessageAsync (); + } + } + + [Test] + public void TestMimeComplianceInvalidHeaderFieldName () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: text/plain +X-Invalid Header: oops, this is invalid + +This is the message body. +"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.InvalidHeader, 7, 1), + }; + + AssertMimeComplianceIssues (text, issues); + } + + [Test] + public Task TestMimeComplianceInvalidHeaderFieldNameAsync () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: text/plain +X-Invalid Header: oops, this is invalid + +This is the message body. +"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.InvalidHeader, 7, 1), + }; + + return AssertMimeComplianceIssuesAsync (text, issues); + } + + [Test] + public void TestMimeComplianceIncompleteHeader () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: text/plain"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.IncompleteHeader, 6, 1) + }; + + AssertMimeComplianceIssues (text, issues); + } + + [Test] + public Task TestMimeComplianceIncompleteHeaderAsync () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: text/plain"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.IncompleteHeader, 6, 1) + }; + + return AssertMimeComplianceIssuesAsync (text, issues); + } + + [Test] + public void TestMimeComplianceInvalidContentTransferEncoding () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary=""boundary-marker"" +Content-Transfer-Encoding: quoted-printable + +--boundary-marker +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: duckduckgo + +This is the message body +--boundary-marker +Content-Type: application/octet-stream; name=""attachment.dat"" +Content-Transfer-Encoding: base64 + + +--boundary-marker-- +"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.InvalidContentTransferEncoding, 7, 1), + new MimeComplianceIssue (MimeComplianceStatus.InvalidContentTransferEncoding, 11, 1) + }; + + AssertMimeComplianceIssues (text, issues); + } + + [Test] + public Task TestMimeComplianceInvalidContentTransferEncodingAsync () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary=""boundary-marker"" +Content-Transfer-Encoding: quoted-printable + +--boundary-marker +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: duckduckgo + +This is the message body +--boundary-marker +Content-Type: application/octet-stream; name=""attachment.dat"" +Content-Transfer-Encoding: base64 + + +--boundary-marker-- +"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.InvalidContentTransferEncoding, 7, 1), + new MimeComplianceIssue (MimeComplianceStatus.InvalidContentTransferEncoding, 11, 1) + }; + + return AssertMimeComplianceIssuesAsync (text, issues); + } + + [Test] + public void TestMimeComplianceInvalidContentType () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: X-ZIP; name=""attachment.zip"" +Content-Transfer-Encoding: base64 + + +"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.InvalidContentType, 6, 1) + }; + + AssertMimeComplianceIssues (text, issues); + } + + [Test] + public Task TestMimeComplianceInvalidContentTypeAsync () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: X-ZIP; name=""attachment.zip"" +Content-Transfer-Encoding: base64 + + +"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.InvalidContentType, 6, 1) + }; + + return AssertMimeComplianceIssuesAsync (text, issues); + } + + [Test] + public void TestMimeComplianceInvalidMimeVersion () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.x +Content-Type: text/plain; charset=us-ascii + +This is the message body. +"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.InvalidMimeVersion, 5, 1) + }; + + AssertMimeComplianceIssues (text, issues); + } + + [Test] + public Task TestMimeComplianceInvalidMimeVersionAsync () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.x +Content-Type: text/plain; charset=us-ascii + +This is the message body. +"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.InvalidMimeVersion, 5, 1) + }; + + return AssertMimeComplianceIssuesAsync (text, issues); + } + + [Test] + public void TestMimeComplianceInvalidWrapping () + { + string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +X-Long-Header: " + new string ('X', 990) + @" + +This is the message body with a really long unwrapped line of text: +" + new string ('X', 1024) + Environment.NewLine; + + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.InvalidWrapping, 7, 1), + new MimeComplianceIssue (MimeComplianceStatus.InvalidWrapping, 10, 1) + }; + + AssertMimeComplianceIssues (text, issues); + } + + [Test] + public Task TestMimeComplianceInvalidWrappingAsync () + { + string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +X-Long-Header: " + new string ('X', 990) + @" + +This is the message body with a really long unwrapped line of text: +" + new string ('X', 1024) + Environment.NewLine; + + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.InvalidWrapping, 7, 1), + new MimeComplianceIssue (MimeComplianceStatus.InvalidWrapping, 10, 1) + }; + + return AssertMimeComplianceIssuesAsync (text, issues); + } + + [Test] + public void TestMimeComplianceMissingBodySeparator () + { + const string text = "From: mimekit@example.org\r\n"; + + var issues = new MimeComplianceIssue[] { + // FIXME: should this technically be raised on line 2? + new MimeComplianceIssue (MimeComplianceStatus.MissingBodySeparator, 1, 1), + }; + + AssertMimeComplianceIssues (text, issues); + } + + [Test] + public Task TestMimeComplianceMissingBodySeparatorAsync () + { + const string text = "From: mimekit@example.org\r\n"; + + var issues = new MimeComplianceIssue[] { + // FIXME: should this technically be raised on line 2? + new MimeComplianceIssue (MimeComplianceStatus.MissingBodySeparator, 1, 1), + }; + + return AssertMimeComplianceIssuesAsync (text, issues); + } + + [Test] + public void TestMimeComplianceMissingMimeVersion () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +Content-Type: multipart/mixed; boundary=""boundary-marker"" + +--boundary-marker +Content-Type: text/plain; charset=us-ascii + +This is the message body. +--boundary-marker +Content-Type: message/rfc822 +Content-Disposition: attachment; filename=""message1.eml"" + +From: mimekit@example.org +To: mimekit@example.org +Subject: This is the first inner test message +Message-Id: <123@example.org> +Content-Type: text/plain; charset=us-ascii + +This is the first inner message body. +--boundary-marker +Content-Type: message/rfc822 +Content-Disposition: attachment; filename=""message2.eml"" + +From: mimekit@example.org +To: mimekit@example.org +Subject: This is the second inner test message +Message-Id: <123@example.org> +Mime-Version: 1.0 +Content-Type: text/plain; charset=us-ascii + +This is the second inner message body. +--boundary-marker-- +"; + var issues = new MimeComplianceIssue[] { + // FIXME: MissingMimeVersion issues are reported with the offset/lineNumber of the start of the message. Should it use a different offset/lineNumber? + new MimeComplianceIssue (MimeComplianceStatus.MissingMimeVersion, 1, 1), + new MimeComplianceIssue (MimeComplianceStatus.MissingMimeVersion, 15, 1) + }; + + AssertMimeComplianceIssues (text, issues); + } + + [Test] + public Task TestMimeComplianceMissingMimeVersionAsync () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +Content-Type: multipart/mixed; boundary=""boundary-marker"" + +--boundary-marker +Content-Type: text/plain; charset=us-ascii + +This is the message body. +--boundary-marker +Content-Type: message/rfc822 +Content-Disposition: attachment; filename=""message1.eml"" + +From: mimekit@example.org +To: mimekit@example.org +Subject: This is the first inner test message +Message-Id: <123@example.org> +Content-Type: text/plain; charset=us-ascii + +This is the first inner message body. +--boundary-marker +Content-Type: message/rfc822 +Content-Disposition: attachment; filename=""message2.eml"" + +From: mimekit@example.org +To: mimekit@example.org +Subject: This is the second inner test message +Message-Id: <123@example.org> +Mime-Version: 1.0 +Content-Type: text/plain; charset=us-ascii + +This is the second inner message body. +--boundary-marker-- +"; + var issues = new MimeComplianceIssue[] { + // FIXME: MissingMimeVersion issues are reported with the offset/lineNumber of the start of the message. Should it use a different offset/lineNumber? + new MimeComplianceIssue (MimeComplianceStatus.MissingMimeVersion, 1, 1), + new MimeComplianceIssue (MimeComplianceStatus.MissingMimeVersion, 15, 1) + }; + + return AssertMimeComplianceIssuesAsync (text, issues); + } + + [Test] + public void TestMimeComplianceMissingMultipartBoundaryParameter () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: multipart/mixed + +--boundary-marker +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +This is the message body +--boundary-marker +Content-Type: application/octet-stream; name=""attachment.dat"" +Content-Transfer-Encoding: base64 + + +--boundary-marker-- +"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.MissingMultipartBoundaryParameter, 6, 1) + }; + + AssertMimeComplianceIssues (text, issues); + } + + [Test] + public Task TestMimeComplianceMissingMultipartBoundaryParameterAsync () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: multipart/mixed + +--boundary-marker +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +This is the message body +--boundary-marker +Content-Type: application/octet-stream; name=""attachment.dat"" +Content-Transfer-Encoding: base64 + + +--boundary-marker-- +"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.MissingMultipartBoundaryParameter, 6, 1) + }; + + return AssertMimeComplianceIssuesAsync (text, issues); + } + + [Test] + public void TestMimeComplianceMissingMultipartStartBoundary () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary=""boundary-marker"" + +--boundary-mark +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +This is the message body +--boundary-mark +Content-Type: application/octet-stream; name=""attachment.dat"" +Content-Transfer-Encoding: base64 + + +"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.MissingMultipartBoundary, 18, 1) + }; + + AssertMimeComplianceIssues (text, issues); + } + + [Test] + public Task TestMimeComplianceMissingMultipartStartBoundaryAsync () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary=""boundary-marker"" + +--boundary-mark +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +This is the message body +--boundary-mark +Content-Type: application/octet-stream; name=""attachment.dat"" +Content-Transfer-Encoding: base64 + + +"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.MissingMultipartBoundary, 18, 1) + }; + + return AssertMimeComplianceIssuesAsync (text, issues); + } + + [Test] + public void TestMimeComplianceMissingMultipartEndBoundary () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary=""boundary-marker"" + +--boundary-marker +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +This is the message body +--boundary-marker +Content-Type: application/octet-stream; name=""attachment.dat"" +Content-Transfer-Encoding: base64 + + +"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.MissingMultipartBoundary, 18, 1) + }; + + AssertMimeComplianceIssues (text, issues); + } + + [Test] + public Task TestMimeComplianceMissingMultipartEndBoundaryAsync () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary=""boundary-marker"" + +--boundary-marker +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +This is the message body +--boundary-marker +Content-Type: application/octet-stream; name=""attachment.dat"" +Content-Transfer-Encoding: base64 + + +"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.MissingMultipartBoundary, 18, 1) + }; + + return AssertMimeComplianceIssuesAsync (text, issues); + } + + [Test] + public void TestMimeComplianceDuplicateContentTransferEncoding () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit +Content-Transfer-Encoding: 8bit + +This is the message body +"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.DuplicateContentTransferEncoding, 8, 1) + }; + + AssertMimeComplianceIssues (text, issues); + } + + [Test] + public Task TestMimeComplianceDuplicateContentTransferEncodingAsync () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit +Content-Transfer-Encoding: 8bit + +This is the message body +"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.DuplicateContentTransferEncoding, 8, 1) + }; + + return AssertMimeComplianceIssuesAsync (text, issues); + } + + [Test] + public void TestMimeComplianceDuplicateContentType () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Type: application/octet-stream + +This is the message body +"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.DuplicateContentType, 7, 1) + }; + + AssertMimeComplianceIssues (text, issues); + } + + [Test] + public Task TestMimeComplianceDuplicateContentTypeAsync () + { + const string text = @"From: mimekit@example.org +To: mimekit@example.org +Subject: This is a test message +Message-Id: <123@example.org> +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Type: application/octet-stream + +This is the message body +"; + var issues = new MimeComplianceIssue[] { + new MimeComplianceIssue (MimeComplianceStatus.DuplicateContentType, 7, 1) + }; + + return AssertMimeComplianceIssuesAsync (text, issues); + } } }