Real mail is full of messages that the RFCs call invalid or leave open, and the established libraries each pick their own
answer. OneMail picks one documented answer per case, reports it as a typed defect, and keeps the original bytes so
nothing is lost whichever answer a caller prefers. This file lists each case, what three released libraries do with it, and
the resolution OneMail takes.
How the table was made
- Each case is a small invented message in
testdata/differential (generated by
tools/corpus/make_differential.py; cases.tsv there describes every file).
- Each message was run through MimeKit 4.18.2, Python 3.13
email (policy.default, message_from_bytes) and Go 1.26
net/mail + mime/multipart, through the drivers in tools/oracles. The columns below are what
those runs showed. Where the three agree, OneMail does the same.
- Where they differ, the rule is: prefer the answer two of the three give; otherwise prefer the answer that loses no
bytes and follows the RFC text most closely. No mail client has been observed for any entry yet, so every resolution
that is not "all three agree" is provisional: it is a default, to be revisited when a client's behaviour is observed.
Provisional entries are marked prov.
- "Defect" is the typed defect the parser raises; every repair and every guess carries the byte offset. Names are the
intended catalogue names;
Ambiguous* marks a choice between valid readings, the others mark a repair.
- Compared forms, normalisation and the difference classes are in
tools/oracles/README.md.
Abbreviations: MK MimeKit, Py Python, Go Go standard library.
Boundaries and multipart structure
| ID | Probe (testdata/differential/…) | MK | Py | Go | Resolution | Defect | Basis |
| B1 | bnd-02 a content line --abcdef when the boundary is abc | content | content | content | A delimiter line is -- + boundary + optional trailing whitespace; a longer line is content. | none | all agree |
| B2 | bnd-03 trailing spaces and tabs after a delimiter | delimiter | delimiter | delimiter | Trailing space and tab are padding (RFC 2046 5.1.1). | none | all agree |
| B3 | bnd-04 --abc xyz: text after the boundary | content | content | content | Not a delimiter; the line stays in the current part. | AmbiguousBoundaryLikeLine | all agree |
| B4 | bnd-14 delimiter in another case (--ABC) | content | content | content | Boundaries are case-sensitive. | none | all agree |
| B5 | bnd-07, bnd-08 one boundary is a prefix of the other | nested correctly | same | same | Longest-exact-match wins: a line closes or starts a part only if it equals a boundary in scope. | none | all agree |
| B6 | bnd-06 text after the closing delimiter, then another --abc | epilogue | epilogue | epilogue | Everything after the closing delimiter is epilogue, including later delimiter-looking lines. | EpilogueHasDelimiter | all agree |
| B7 | bnd-05 closing delimiter missing | last part keeps its final line break (5 bytes) | strips it (3) | strips it (3) | The last part runs to end of input and keeps every byte; no line break is assumed to belong to a delimiter that is not there. | MissingFinalBoundary | prov. (MK; keeps all bytes) |
| B8 | bnd-09 multipart with no boundary | multipart, no parts, body not exposed | leaf, body kept | leaf, body kept | Treated as a leaf of the declared type with the body kept; no parts. | MultipartWithoutBoundary | 2 of 3 |
| B9 | bnd-10 boundary="" | no parts | leaf | leaf | Same as B8. | MultipartWithoutBoundary | 2 of 3 |
| B10 | bnd-25 multipart whose body has no delimiter at all | no parts | leaf, body kept | no parts | No parts; the whole body is the preamble, bytes preserved. | MissingStartBoundary | 2 of 3 |
| B11 | bnd-11 boundary parameter that ends in a space | none found | finds a part | 2 parts | The boundary is matched literally and also with trailing space trimmed. | BoundaryHasTrailingSpace | prov. |
| B12 | bnd-12 unquoted boundary=a/b=c | accepted whole | cut at = | media type rejected | The unquoted value runs to ; or end of value. | none (not raised) | prov. (MK) |
| B13 | bnd-13 two boundary parameters | first | first | media type rejected (leaf) | First occurrence wins. | DuplicateParameter | 2 of 3 |
| B14 | bnd-15 multipart with only --abc-- | 0 parts | leaf, empty | 0 parts | 0 parts. | none | 2 of 3 |
| B15 | bnd-16 first delimiter directly after the headers, no blank line | header block never ends, 0 parts | 1 part | header error | The first line that is not a header field or continuation ends the header block and starts the body (see H5). | MissingHeaderBodySeparator | prov. (Py) |
| B16 | bnd-17 LF-only delimiters inside a CRLF message | 2 parts | 2 parts | 1 part | Each line may end in LF or CRLF independently. | MixedLineEndings | 2 of 3 |
| B17 | bnd-18 part with headers and no blank line before the next delimiter | 2 parts | 2 parts | 0 parts | Part with headers and an empty body. | MissingHeaderBodySeparator | 2 of 3 |
| B18 | bnd-23 delimiters separated by bare CR | no parts | 1 part | no parts | A bare CR is not a line break (see L1). | BareCarriageReturnInHeader | 2 of 3 |
| B19 | bnd-26 inner multipart reuses the outer boundary | nested | outer swallows inner | nested | The innermost open multipart is tried first, so the shared line closes the inner part. | AmbiguousDuplicateBoundary | prov. (MK, Go) |
| B20 | bnd-19, bnd-20, bnd-21, bnd-22, bnd-24 empty part headers; blank line before delimiter; delimiter at end of file without line break; whitespace-only preamble and epilogue; 80-character boundary | same | same | same | The line break before a delimiter belongs to the delimiter; a part can have no headers (defaults apply); the boundary length limit is not enforced; whitespace-only preamble/epilogue counts as present but empty. | BoundaryTooLong for over 70 | all agree |
Line endings and raw octets
| ID | Probe | MK | Py | Go | Resolution | Defect | Basis |
| L1 | eol-02 whole message with CR-only line ends; eol-05 CR inside a header value; eol-09 CR CR LF | CR-only: one long header; CR in value dropped from the decoded value | CR is a line break | CR kept in the value | A line ends at LF or CRLF only. A lone CR is data: it stays in the raw value and is dropped from the decoded value. | BareCarriageReturnInHeader | prov. (MK, Go) |
| L2 | eol-01, eol-03, eol-04, eol-06, eol-07, eol-08 LF-only; mixed CRLF/LF; no final newline; headers end at EOF | same | same | same | Accept any mix of LF and CRLF; a body's bytes are never changed. | MixedLineEndings when mixed | all agree |
| L3 | raw-01, raw-09 NUL, form feed and vertical tab in a header value | kept | kept | kept | Kept in raw and decoded values. | ControlCharacterInHeader | all agree |
| L4 | raw-02 NUL in a header name | header | body starts here | header | Accepted as a header name; the raw bytes are kept. | InvalidHeaderName | 2 of 3 |
| L5 | raw-03, raw-07, raw-08 NUL in a body; 3000-character header; 20000-character body line | kept | kept | kept | No length limits below the configured resource limits; the defect LineTooLong marks lines over 998. | LineTooLong | all agree |
| L6 | raw-04, raw-05, raw-11, adr-24 raw 8-bit octets in headers | UTF-8, else Latin-1 | U+FFFD for non-ASCII | invalid bytes become U+FFFD; one address list fails | A raw 8-bit header value is decoded as UTF-8 if valid, otherwise as Windows-1252 (RFC 6532 for the UTF-8 case). | AmbiguousEightBitHeader | prov. (MK) |
| L7 | raw-10 UTF-8 byte-order mark before the first header | header found | body starts here | the BOM stays in the first name, which then goes unrecognised | The BOM is ignored when the name is read (the raw bytes keep it), so the first field is found. | InvalidHeaderName | prov. (MK) |
| L8 | raw-06, cte-14, ct-14 8-bit body declared 7bit or without charset | bytes kept | bytes kept | bytes kept | Body bytes are never altered; charset choice is the encodings layer's. | EightBitIn7BitBody | all agree |
| ID | Probe | MK | Py | Go | Resolution | Defect | Basis |
| H1 | hdr-01, hdr-02, hdr-03 duplicate Content-Type, MIME-Version, Content-Transfer-Encoding | first | first | first | The first occurrence is the one in force; all stay in the tree. | DuplicateHeader | all agree |
| H2 | hdr-04, adr-25 duplicate From, To, Subject, Date | From/To lists merge; all Subjects kept | first | first | The typed accessors use the first occurrence; every header stays in the tree. | DuplicateHeader | 2 of 3 |
| H3 | hdr-07 whitespace before the colon (Subject : x) | header | body starts here | header | Accepted; the name is trimmed (RFC 5322 4.2 obsolete syntax). | WhitespaceBeforeHeaderColon | 2 of 3 |
| H4 | hdr-14 space inside a header name | header | body starts here | header | Accepted as written; the name is kept (Latin-1) and InvalidHeaderName is reported. | InvalidHeaderName | 2 of 3 |
| H5 | hdr-05, hdr-06, hdr-09 a line without a colon in or after the headers | line skipped (or parse error on the first line) | body starts at the line | error | The first non-header line ends the header block and starts the body, so no byte is dropped. | MissingHeaderBodySeparator | prov. (Py) |
| H6 | hdr-10 body line that looks like a header (Note: x) with no blank line | header | header | error | It is a header. | MissingHeaderBodySeparator | all agree where parsed |
| H7 | hdr-08 continuation line before the first header | parse error | ignored | error | A line that starts with white space where a field should begin ends the header block and starts the body. | HeaderContinuationWithoutHeader | prov. |
| H8 | hdr-11 From envelope line first | skipped | skipped | skipped | Kept as the entity's envelope line (its own reader event), not a header; the header block starts after it. | none | all agree |
| H9 | hdr-12 blank lines before the first header | empty header block | empty header block | empty header block | An empty header block; everything after is the body. | NoHeaders | all agree |
| H10 | hdr-16, hdr-15, hdr-21 folding with tabs, runs of spaces and blank continuation lines | keeps white space | keeps white space | collapses to one space | Unfolding removes the line break only; interior white space stays. | none | 2 of 3 |
| H11 | hdr-25 trailing white space on a value | trimmed | kept in the decoded value | trimmed | Trimmed in the decoded value, kept in raw. | none | 2 of 3 |
| H12 | hdr-18 empty file (hdr-19, hdr-20: blank line only and headers only agree) | error | empty message | error | An empty message; the parser never throws. | EmptyMessage | prov. (Py) |
| H13 | hdr-13, hdr-17, hdr-22, hdr-23, hdr-24, hdr-26 empty values, no MIME-Version, colons in values, odd case, 2000 headers, trace order | same | same | same | Names compare case-insensitively; a missing MIME-Version does not stop MIME parsing; order is preserved. | MissingMimeVersion | all agree |
| H14 | nest-13 message cut inside a header name | error | body | error | The partial line is kept as the start of the body. | TruncatedHeader | prov. (Py) |
Content-Type
| ID | Probe | MK | Py | Go | Resolution | Defect | Basis |
| C1 | ct-02, ct-03, ct-04, ct-09 text without subtype, text/, garbage, empty value | application/octet-stream | text/plain (text/ kept literal for ct-03) | text/plain or text | A value that is not type/subtype is treated as absent: text/plain; charset=us-ascii, raw header kept. | InvalidContentType | 2 of 3 |
| C2 | ct-05 parameter without = | dropped | charset="" | dropped | Dropped. | InvalidParameter | 2 of 3 |
| C3 | ct-07 comments in the value | one comment stays in the value | comment stays in the type | removed | Comments are removed (RFC 2045 5.1). | none | prov. (Go) |
| C4 | ct-01, ct-06, ct-08, ct-10, ct-11, ct-12 odd case and spacing, unterminated quote, duplicate charset, ;;, unknown charset, raw UTF-8 in a parameter | same | same | same | Lower-case types and parameter names; first duplicate wins; an unterminated quote runs to the end of the value; extra semicolons are ignored; unknown charsets are kept as written. | UnterminatedQuote, DuplicateParameter | all agree |
| C5 | ct-13 top-level message/rfc822 | nested message | nested message | nested message | Parsed as a message. | none | all agree |
Content-Transfer-Encoding
| ID | Probe | MK | Py | Go | Resolution | Defect | Basis |
| E1 | cte-01, cte-04, cte-19, cte-17, cte-12, cte-10, cte-11 unknown, odd-case, empty, binary; QP lower-case hex, soft breaks, invalid escapes | same | same | same | Unknown, empty, 7bit, 8bit, binary: identity. Names are case-insensitive and trimmed. QP: invalid escapes pass through as written. | UnknownTransferEncoding, InvalidQuotedPrintable | all agree |
| E2 | cte-02, cte-03 composite type declared base64 or 8bit | parts parsed | parts parsed | decodes first, no parts | The declared encoding is ignored for multipart/*: parts are parsed from the raw body. | AmbiguousEncodedComposite | 2 of 3 |
| E3 | cte-05 base64 with characters outside the alphabet | 9 bytes | 10 bytes | 3 bytes | Characters outside the alphabet are skipped, the rest is decoded as one stream. | InvalidBase64 | prov. (Py) |
| E4 | cte-06 base64 without padding | drops the last partial group (9) | decodes it (11) | drops it (9) | The final partial group is decoded. | MissingBase64Padding | prov. (Py; keeps data) |
| E5 | cte-07, cte-20 white space and line breaks inside base64 | skipped | skipped | stops at white space | White space is skipped anywhere. | none | 2 of 3 |
| E6 | cte-08 data after = padding | continues | continues | stops | Decoding continues after padding; each padded run is decoded on its own. | DataAfterBase64Padding | 2 of 3 |
| E7 | cte-13 trailing spaces before a QP line end | kept | kept | stripped | Kept (they are data in transit; RFC 2045 6.7 rule 3 only forbids producing them). | none | 2 of 3 |
| E8 | cte-15 message/rfc822 declared base64 | not decoded | decoded, parsed | decoded, parsed | Stays a leaf whose body is the encoded bytes (decoding it first would break the byte-for-byte tree). | AmbiguousEncodedComposite | prov. (MK) |
| E9 | cte-16 uuencode | decoded | decoded | not decoded | Decoded (x-uuencode, uuencode, x-uue, uue). | none | 2 of 3 |
| E10 | cte-18 comment after the name (base64 (note)) | decoded | decoded | not decoded | Comments are removed first. | none | 2 of 3 |
Parameters, encoded words and file names
| ID | Probe | MK | Py | Go | Resolution | Defect | Basis |
| N1 | enc-01, enc-30 RFC 2047 encoded word inside a quoted or token parameter value | decoded | decoded (quoted only) | literal | Decoded, although RFC 2047 5 forbids it in parameters; every mail client does. | AmbiguousEncodedWordInQuotedString | 2 of 3 |
| N2 | enc-04 filename= and filename*= together | extended | plain | extended | The extended (RFC 2231) value wins. | AmbiguousParameterForms | 2 of 3 |
| N3 | enc-02, enc-03, enc-09, enc-05 RFC 2231 charset, continuations, out-of-order continuations | same | same | same | Continuations are joined by index before charset decoding (RFC 2231 3). | none | all agree |
| N4 | enc-06 unknown charset in RFC 2231 | decoded as Latin-1 | U+FFFD | parameter dropped | Fall back to Windows-1252. | AmbiguousUnknownCharset | prov. |
| N5 | enc-07 filename*= without the charset quote marks | Latin-1 mojibake | UTF-8 | parameter dropped | Percent-decode, then UTF-8 if valid, else Windows-1252. | MalformedExtendedParameter | prov. (Py) |
| N6 | enc-08 continuations starting at *1 | joined | dropped | dropped | Joined in index order. | AmbiguousParameterSegments | prov. (MK) |
| N7 | enc-14 two filename parameters | first | first | parameter dropped | First wins. | DuplicateParameter | 2 of 3 |
| N8 | enc-15 unquoted file name with a space | whole value | cut at the space | rejected | The value runs to ; or end of value. | none (not raised) | prov. (MK) |
| N9 | enc-10, enc-11 name versus filename | disposition filename, else type name | same | same | Disposition filename first, then Content-Type name. | none | all agree |
| N10 | enc-12, enc-13 path separators, .., backslash escapes | as written | as written | as written | The name is kept as written; making it safe to write to disk is the caller's or the export layer's job. | UnsafeFilename | all agree |
| N11 | enc-16–enc-20, enc-22–enc-24, enc-28, enc-29 encoded words: Q and B, adjacent, mixed with text, unknown charset, over-long, split by a fold, empty, in Comments/Keywords | same | same | same | RFC 2047 decoding; white space between adjacent encoded words is dropped. | EncodedWordTooLong, AmbiguousSplitMultibyte | all agree |
| N12 | enc-21 encoded word with invalid base64 | word removed | word replaced by a space | kept literally | Left in the text as written (RFC 2047 6.3 allows displaying it as ordinary text). | InvalidEncodedWordBase64, InvalidEncodedWordQuotedPrintable | prov. (Go) |
| N13 | enc-25 encoded word in a quoted display name | decoded | decoded | literal | Decoded. | AmbiguousEncodedWordInQuotedString | 2 of 3 |
| N14 | enc-27 ISO-2022-JP encoded word | escape sequences left | decoded | not decoded | Decoded. | none | prov. (Py) |
Addresses
| ID | Probe | MK | Py | Go | Resolution | Defect | Basis |
| A1 | adr-01, adr-02, adr-27 group syntax, empty group | flattened | flattened | flattened | Groups are kept as groups in the model; the shared view flattens members. | none | all agree |
| A2 | adr-03 unquoted comma in a display name | one mailbox, name Doe, John | two: Doe (no domain) and John | list rejected | One mailbox named Doe, John: a comma before < stays in the phrase. | AmbiguousUnquotedCommaInPhrase | prov. (MK) |
| A3 | adr-04 bare address after plain words | second mailbox dropped | second mailbox with a wrong local part | list rejected | The words become the display name and the address is kept. | MissingAngleBrackets | prov. |
| A4 | adr-05 name given as a trailing comment | name from the comment | no name | name from the comment | The comment becomes the display name when there is none. | none | 2 of 3 |
| A5 | adr-06, adr-07, adr-20 nested comments, obsolete source route, spaces around @ and . | same | same | list rejected | Comments are stripped, the route is dropped, CFWS inside the address is removed. | ObsoleteRoute | 2 of 2 that parse |
| A6 | adr-08 quoted local part | quotes kept | quotes kept | unquoted | The address keeps its quoting as written; an unquoted form is available alongside. | none | 2 of 3 |
| A7 | adr-09 empty address <> | mailbox dropped | kept, address <> | list rejected | A mailbox with an empty address and its name is kept. | EmptyAddress | prov. |
| A8 | adr-10 local part without a domain | kept | kept | rejected | Kept as written: john and mary@ are two mailboxes, each with MissingDomain (dropping a recipient loses data). | MissingDomain | prov. |
| A9 | adr-11, adr-13, adr-14, adr-16, adr-21, adr-22, adr-26, adr-28 IP literals; empty list elements; unclosed <; dotted names; 50 entries; escaped quoted names; several From; empty Cc/Bcc | same | same | mostly same | Accepted as written; empty elements are skipped; an unclosed < is closed at the end of the value. | UnclosedAngle | all agree where parsed |
| A10 | adr-12, adr-24 raw UTF-8 or Latin-1 in an address | name decoded; local part with UTF-8 dropped | U+FFFD | rejected | RFC 6532: UTF-8 in local part and domain is accepted; names follow L6. | AmbiguousEightBitHeader | prov. |
| A11 | adr-15 unclosed quoted name | name John, address kept | whole value as address | rejected | Recover as MimeKit does: the open quote ends at the <. | UnterminatedQuote | prov. (MK) |
| A12 | adr-17 @ inside an unquoted display name | name kept | wrong address | rejected | The address is the one in angle brackets. | none | prov. (MK) |
| A13 | adr-18 two address-specs in one mailbox (<a@x><b@y>) | both | first only | rejected | Both are kept as two mailboxes. | AmbiguousAdjacentAngleAddrs | prov. (MK) |
| A14 | adr-19 semicolon as a separator | first only | first only | rejected | Only the first address is parsed; the remainder is kept as unparsed text in the defect. | UnparsedAddressText | 2 of 2 that parse |
| A15 | adr-23 leading, trailing and doubled dots in a local part (.a@x, b.@x, c..d@x) | address dropped | kept | rejected | Kept as written. These are common in old corporate mail, and dropping them loses recipients. | ObsoleteLocalPart | prov. (Py; in about 9% of messages in a 517,000-message public corpus, MimeKit drops such recipients) |
| A16 | gateway-rewritten addresses seen in the Enron corpus: <"a"@b.com@gw>, munn"."mary@gw, "d@x".@gw@gw | MimeKit keeps the part up to the second @, or drops the address; Go rejects the list | Python gives <> or a mangled address | Kept as written, with InvalidAddressSyntax. | InvalidAddressSyntax | prov. (no common reading) |
Dates
All forms are read into an instant plus the written zone; the source text is kept with the value.
| ID | Probe | MK | Py | Go | Resolution | Defect | Basis |
| D1 | date-01–date-13, date-18, date-24, date-26, date-28, date-33 standard, no seconds, no weekday, wrong weekday, EST/GMT/UT, comment zone, lower-case month, extra white space, ±14:00 | same | same | same | Accept; the weekday is ignored; named zones follow RFC 5322 4.3 (EST −5, PDT −7 …); with two Date headers the first is used. | WeekdayMismatch | all agree |
| D2 | date-34–date-37, date-03–date-05 two-digit years | 00–68 → 20xx; 69 → 2069; 70+ → 19xx | 00–68 → 20xx; 69–99 → 19xx | same as Py | 00–68 → 2000–2068, 69–99 → 1969–1999. All three libraries read 50 as 2050, which differs from RFC 5322 4.3 (50–99 → 19xx); OneMail follows the libraries. | AmbiguousTwoDigitYear | 2 of 3 (69) |
| D3 | date-14 three-digit year | rejected | year 101 | rejected | Add 1900, as RFC 5322 4.3 says (101 → 2001). | ThreeDigitYear | prov. (RFC only) |
| D4 | date-10, date-12, date-21, date-25 -0000, military letters, GMT+2, missing zone | UTC | UTC, zone unknown for -0000 and none | Z and none rejected; GMT+2 read as +2 | UTC with the zone marked unknown (RFC 5322 4.3: military zones are unreliable and read as -0000). | UnknownTimeZone | 2 of 3 |
| D5 | date-16 seconds = 60 | 59 | rejected | rejected | Accept as a leap second, instant clamped to :59. | LeapSecond | prov. (MK) |
| D6 | date-17 zone +2400 | UTC | rejected | +24:00 | Out-of-range zones are read as unknown. | InvalidTimeZone | prov. |
| D7 | date-19 2001-05-14 23:39:00 | parsed as UTC | rejected | rejected | Rejected (not RFC 5322); the header stays as text. | InvalidDate | 2 of 3 |
| D8 | date-22 hour 24 | wrong date (00:00 on the 14th) | rejected | rejected | Rejected. | InvalidDate | 2 of 3 |
| D9 | date-27 year 1899 | rejected | accepted | accepted | Accepted. | none | 2 of 3 |
| D10 | date-29 PDT | −7 | −7 | rejected | Accepted (obsolete zone, RFC 5322 4.3). | none | 2 of 3 |
| D11 | date-30 fractional seconds | wrong time (00:00) | rejected | accepted | Fraction dropped. | FractionalSeconds | prov. (Go) |
| D12 | date-15, date-20, date-23, date-31, date-32 garbage, ISO with T…Z, 31 April, empty, day 0 | rejected | rejected | rejected | Rejected; the value reads as absent and the header stays as text. | InvalidDate | all agree |
| D13 | date-38 four-digit year with leading zeros (0002), which a client that once wrote a two-digit year can produce | 2002 | 2002 | year 2 | Read as a two-digit year (rule D2). | AmbiguousTwoDigitYear | 2 of 3 |
Nested messages and structure
| ID | Probe | MK | Py | Go | Resolution | Defect | Basis |
| M1 | nest-01, nest-02, nest-05, nest-17 message/rfc822 plain, multipart inside, 8bit, 30 levels | nested | nested | nested | Parsed recursively, bounded by the nesting limit. | none | all agree |
| M2 | nest-03, nest-04 message/rfc822 that is empty or has no headers | message part without inner message, or body in an empty inner message | inner message with empty body | no inner message | An inner message exists whenever the part is message/rfc822; text without headers becomes its body. | NoHeaders | prov. |
| M3 | nest-06, nest-07 multipart/digest children without a type | message/rfc822 | message/rfc822 | text/plain | message/rfc822 (RFC 2046 5.1.5). | none | 2 of 3 |
| M4 | nest-08, nest-09 message/delivery-status, message/partial | bytes | groups parsed as headers | bytes | Bytes are kept; the status groups are parsed on demand. | none | 2 of 3 |
| M5 | nest-12 cut inside a part's header | partial part | partial part | no part | The part exists with its partial header. | TruncatedHeader | 2 of 3 |
| M6 | nest-14, nest-15, nest-16 10 and 200 nested multiparts, 3000 parts | all parsed | all parsed | all parsed | None of the three limits depth (200) or part count (3000). OneMail's limits are on by default and are listed in the resource-limit notes. | DepthLimitExceeded, EntityLimitExceeded | no oracle limit |
| M7 | nest-10, nest-11, nest-18, nest-19, nest-20, nest-21 alternative/related, signed, QP HTML, inline and unknown dispositions, a lying Content-Length, mbox-like trailer lines | same | same | same | Structure as written; Content-Length is advisory and ignored; an unknown disposition is kept as written. | none | all agree |
Defect names in this table that are not raised, and why
The decoders and the tree keep what they repair without a defect where the repair is the specified reading, or report it by another route.
| Name in the table | Where it is reported instead |
InvalidBase64, MissingBase64Padding, DataAfterBase64Padding, InvalidQuotedPrintable (E1, E3, E4, E6) | Body decoding is lazy, so the decoders report TransferIssues flags on the decoding stream, not tree defects. |
DuplicateHeader (H1, H2) | Not raised: all copies stay in the tree; only a repeated Content-Type or Content-Transfer-Encoding is a defect (AmbiguousDuplicateContentType, AmbiguousDuplicateTransferEncoding). |
NoHeaders, EmptyMessage, TruncatedHeader (H9, H12, H14, M2) | Not raised: an empty or cut-off message is a valid empty tree; MissingHeaderBodySeparator marks a header block that ran into the end of input. |
MissingMimeVersion (H13) | Not raised: a missing MIME-Version does not change parsing. |
LineTooLong, EightBitIn7BitBody (L5, L8) | Not raised: body bytes are never judged. |
EnvelopeLine (H8) | Not a defect: the reader emits an EnvelopeLine event. |
UnsafeFilename (N10) | Not raised: a file name is kept as written. |
EncodedMessagePart (E8) | Reported as AmbiguousEncodedComposite. |
UnquotedParameterSpecials (B12, N8) | Not raised: an unquoted value runs to ;. |
Differences the differential run reports, and how each is explained
tools/oracles/explained.tsv maps every difference between OneMail and each oracle to an entry above, to a documented normalisation of
the comparison (NORM-*) or to a named limitation of that oracle (LIMIT-*). A difference with no line fails the run.
Observed black-box on Python icalendar 7.3.0 (Py-ical) and vobject 0.9.9 (Py-vobj), over the public corpus and our probes (tools/calendar/oracles). Ical.Net 5.2.3 and FolkerKinzel.VCards 8.2.0 expose typed models only, so they settle value questions, not line syntax. The tree always keeps every byte (untouched input writes back identical); the rows below govern the decoded view and the defects. Every row is our default, marked provisional where fewer than two oracles agree.
| ID | Case | Py-ical | Py-vobj | Resolution | Defect | Basis |
| K1 | Line ends: LF only, lone CR | accepts LF | accepts LF | CRLF, LF and lone CR all end a line. Reported once per file, at the first. | BareLineFeed, BareCarriageReturn | all agree for LF; CR prov. |
| K2 | Fold with two whitespace characters after the break | one removed (ab cd) | one removed | Exactly one space or tab is removed (RFC 5545 §3.1); the rest is content. | none | all agree |
| K3 | Fold inside a multi-byte UTF-8 sequence | n/a | n/a | Lines are unfolded as bytes before decoding, so the character is whole. | none | RFC 5545 §3.1 |
| K4 | Line with no colon | skipped | parse error | Kept in place as a malformed line (written back as read); not a property. | MissingColon | prov. (lossless) |
| K5 | Space before the colon (SUMMARY :x) | accepts | parse error | Accepted; the name is trimmed. | WhitespaceAroundDelimiter | prov. (1 of 2) |
| K6 | Lower-case names and begin:vcalendar | accepts | accepts | Names and component names compare ASCII case-insensitively; case is kept as written. | none | all agree |
| K7 | END: that does not match the open component | error | error | Closes up to the matching open component; the components in between are left unclosed. With no match, the line is kept as malformed. | MissingEnd, UnmatchedEnd | prov. (both reject; ours must not) |
| K8 | RFC 6868 caret sequences in a parameter value (^n, ^^, ^') | decoded | literal | Decoded. | AmbiguousCaretEncoding | prov. (1 of 2) |
| K9 | The same parameter twice (ROLE=A,B;ROLE=C) | last wins | merged | Both are kept in order as written; readers that want one value take the first. | none | lossless |
| K10 | Unknown text escape (\q, \", \:) | \q and \" kept, \: becomes : | \q and \: kept, \" becomes " | The backslash and the character stay as written. Only \\ \; \, \n \N are resolved (vCard 2.1: \\ \;). | none | prov.; \q agrees |
| K11 | Escapes in X- properties | raw | unescaped | Unescaped like any TEXT value (RFC 5545 §3.8.8.2 gives X- properties the TEXT type). | none | prov. (1 of 2) |
| K12 | Invalid UTF-8 text | U+FFFD | U+FFFD | The line is read as Windows-1252, the reading clients use for legacy files, unless CHARSET names a charset we can decode. The source bytes are untouched. | AmbiguousNonUtf8Text | prov. (our default) |
| K13 | vCard 2.1 CHARSET we cannot decode | n/a | n/a | Read as UTF-8, or Windows-1252 when that is invalid. | UnknownCharset | our default |
| K14 | vCard 2.1 quoted-printable soft break (= at line end) | n/a | joined | Joined whenever the line is marked QUOTED-PRINTABLE, in vCard and ICS alike. | QuotedPrintableSoftBreak | prov. |
| K15 | vCard 2.1 base64 data on unindented lines, ended by an empty line | n/a | joined | Joined when the line carries ENCODING=BASE64/B/bare BASE64 and the next line holds only base64 characters. The ending empty line belongs to the property. | Base64ContinuationWithoutFold | prov. |
| K16 | vCard 2.1 bare parameters (TEL;WORK;VOICE) | n/a | dropped on some inputs | Kept as bare parameters; the typed view lists them as types together with TYPE= values. | none | lossless |
| K17 | Comma lists with an empty element (ADR street 5,) | n/a | element dropped | The empty element is dropped in the typed view; the raw value keeps it. | none | prov. |
| K18 | Properties inside VCALENDAR after BEGIN:VCARD junk, stray lines around components | n/a | n/a | Kept as items of the document root, in order. | MissingColon, UnmatchedEnd | lossless |
| K19 | A date-time with a TZID and a Z suffix | UTC | UTC | UTC wins; the TZID stays a parameter. | none | all agree |
| K20 | Nesting beyond 32 levels | unlimited | unlimited | Deeper BEGIN/END lines are kept as ordinary properties. | DepthLimitExceeded | limit |
| CV1 | Unknown escape in a typed vCard 3.0/4.0 value (\:, \", \q) | vobject drops the backslash of \" only; FolkerKinzel drops that of \: only | | The typed view (not the raw value) drops the backslash and keeps the character, for every character after \\ \; \, \n \N. vCard 2.1 unescapes \\ and \; only. | none | prov. (oracles disagree) |
| CV2 | TEL;TYPE=VOICE,MSG,WORK imported to a contact | n/a | n/a | Every matching rule fires: Business and Other (MS-OXVCARD 2.1.3.4.1 example, literally). A fax alone fills Primary Fax, pref fills Primary, no type fills Other. | none | spec example; no client observed |
| CV3 | ADR with no recognised type | n/a | n/a | Business and Other; postal, dom, intl, parcel fill Other; a fourth address is dropped. | AdditionalValueNotMapped | spec example; prov. |
| CV4 | A second or later TEL/EMAIL of a kind with one slot | n/a | n/a | The second work (home) number goes to Business2 (Home2); later ones are reported, not stored. The first three emails fill the slots in card order. | AdditionalValueNotMapped, and on export PhoneSlotChanged, EmailSlotChanged | spec; prov. |
| CV5 | BDAY as a partial date or with a time | n/a | n/a | PidTagBirthday is the date at 11:59 UTC, PidLidBirthdayLocal the date at 0:00 (Outlook's own pair); the time of day is dropped; a date without a year is not stored. | TimeOfDayDropped, PartialDate | spec 2.1.3.2.5; prov. |
| CV6 | An empty text property in a contact (PidTagDepartmentName = "") | n/a | n/a | Treated as absent: no vCard property is written and the report says so. | EmptyValueNotWritten | agent default |
| CV7 | CR CR LF in a vCard | tolerated by vobject | n/a | Two line ends (K1); a folded value that contains it is cut, as the reader does for any line end. A reader-level decision is open. | none | prov. |
Observed black-box on ical.js 2.2.1, Python icalendar 7.3.0, biweekly 0.6.8 and ez-vcard 0.12.2 (tools/calendar/oracles/forms); the rows say how we resolve what the RFCs leave open or what the oracles do differently. Reasons and the full class list: jcal-xcal.md and explained.tsv.
| ID | Case | Oracles | Resolution | Defect or loss | Basis |
| J1 | Value that does not fit its type (DTSTAMP:19970901T1300Z, TRIGGER:19980403T120000) | ical.js, biweekly print a guessed, often garbled typed value | Carried as unknown with the raw text; the declared VALUE stays as a parameter, so the text writes back as read | ValueNotRepresentable | lossless |
| J2 | DTSTART:19971102 with no VALUE=DATE | ical.js, biweekly: date-time with a garbled time | A date (RFC 7265 Appendix B.1 shows it) | AmbiguousValueType | RFC example |
| J3 | ENCODING=BASE64 without VALUE=BINARY on ATTACH or an extension property; VALUE=BINARY without ENCODING | n/a | Both are binary; ENCODING=BASE64 is added in the second case | AmbiguousValueType | RFC 5545 §3.3.1 |
| J4 | TZ:-0500 | ical.js text | utc-offset (RFC 7095 Appendix B.1); VALUE is not written back | none | RFC example |
| J5 | Parameter with no value (ATTENDEE;MAILTO:) | ical.js, icalendar drop it | One empty value, in JSON and XML | none | lossless |
| J6 | Parameter given twice | ical.js, icalendar keep the last | Values merged in order | DuplicateParameterMerged | lossless |
| J7 | Same JSON object key twice | n/a | Both kept | DuplicateKey | lossless |
| J8 | TYPE, PID, SORT-AS values, also a quoted "a,b" | ez-vcard keeps one string | Split into arrays (RFC 7095 §3.3.1.1) | none | RFC |
| J9 | Inline vCard base64 (PHOTO, KEY) | ical.js keeps binary and ENCODING | data: URI; media type from TYPE, else application/octet-stream | InlineBinaryAsDataUri | RFC 7095 §3.5, RFC 2397 |
| J10 | Backslash escapes in a vCard URI value (URL:http\://x) | ical.js and ez-vcard unescape | Unescaped (\\ \, \; \:); a backslash cannot occur in a URI | none | 2 of 2 |
| J11 | vCard 2.1/3.0 data in xCard | ez-vcard writes 4.0 everywhere | 4.0 forms for GEO, TYPE=pref; VERSION dropped. jCard keeps the source version and type names | VersionNotCarried, VersionSpecificForm | RFC 6351 |
| J12 | Group prefix on iCalendar data | ical.js keeps GROUP.NAME as the name | Split into the group parameter | GroupNotCarried in XML | RFC 7095 §3.3.1.2 |
| J13 | Property, parameter, component or type name that is not an XML name | biweekly, ez-vcard drop or rename | Left out of xCal/xCard (JSON carries it) | PropertyNotCarried, ParameterNotCarried, ComponentNotCarried, TypeNameNotCarried | XML 1.0 |
| J14 | Character XML cannot hold (U+000C, NUL, lone surrogate) | n/a | U+FFFD | InvalidXmlCharacter | XML 1.0 |
| J15 | vCard 2.1 quoted-printable =0D=0A | ez-vcard keeps CR LF in LABEL | Line feed in the form; the text writer uses quoted-printable again and never folds a line after = | none | RFC 6350 §3.4 |
| J16 | Components outside a VCALENDAR (bare VEVENT examples) | biweekly writes calendars only | Carried at the top level of the form | none | lossless |
| J17 | Durations, UTC offsets, TZID values, repeated FREEBUSY periods, URI scheme case, local times with TZID | icalendar, biweekly normalise (P7W to P49D, -00:00 to +00:00, local to UTC, MAILTO: to mailto:) | Kept as written | none | lossless |
Recurrence and time zones
Ours against recurring-ical-events 3.8.2 on icalendar 7.3.0 (rec), python-dateutil 2.9.0.post0 rrule through our glue (du) and Ical.Net 5.2.3 (ical), black-box, over every recurring component of the public calendar corpus plus our generated fixtures: 2,384 components, each in a fixed window (tools/calendar/run_expand_diff.sh; method in docs/recurrence.md). A component is identical when the sorted start lists are equal, differing otherwise, not expanded when the oracle errors or times out (330 libical components have an invalid DTSTART that every oracle rejects). Counts are components. Each difference gets exactly one id, the first cause that applies; the id is the entry below. Basis as in the other sections: all agree, 2 of 3, or provisional (our default).
| Oracle | Compared | Identical | Differing | Explained | Unexplained | Not expanded |
| rec | 2,372 | 2,233 | 139 | 139 | 0 | 12 (2 broken property, 8 timeout, 2 value error) |
| du | 2,378 | 2,238 | 140 | 140 | 0 | 6 (1 broken property, 3 type error, 2 value error) |
| ical | 2,188 | 1,409 | 779 | 779 | 0 | 196 (130 timeout, 33 argument, 30 out of range, 3 other) |
| ID | Case | Oracles | Resolution | Defect | Differences (rec / du / ical) | Basis |
| R1 | BYWEEKNO with no BYDAY, BYMONTHDAY or BYYEARDAY | rec and du: every day of the week; Ical.Net: the weekday of DTSTART | The weekday of DTSTART (RFC 5545 §3.3.10: a finer part that is absent takes DTSTART's value). | none | 32 / 32 / 0 | prov. (RFC; Ical.Net agrees) |
| R2 | A part the §3.3.10 table marks N/A for the frequency (BYWEEKNO with MONTHLY, BYYEARDAY with DAILY/WEEKLY/MONTHLY, BYMONTHDAY with WEEKLY, BYSETPOS alone, ordinal BYDAY where §3.3.10 forbids it) | the three disagree with each other and with us on some; 3 of the Python differences are one occurrence at the COUNT boundary of WEEKLY;BYMONTHDAY;BYSETPOS;COUNT | The part filters, never expands. | none | 102 / 102 / 449 | prov. (the table gives no result) |
| R3 | DTSTART does not fit its own rule (§3.8.5.3: undefined) | rec and du include DTSTART; Ical.Net omits it | DTSTART is always the first instance and counts toward COUNT. | AmbiguousUnsynchronizedStart | 0 / 0 / 75 (rec and du give our list on all 94 such components, apart from those with an R1 or R2 part) | 2 of 3 |
| R4 | UNTIL of another value type than DTSTART (DATE vs date-time, UTC vs floating) | the oracles differ with each other | A UTC UNTIL is an instant for zoned and UTC series and a wall-clock value for floating ones; a DATE UNTIL covers its whole day. | AmbiguousUntilType | 2 / 1 / 1 | prov. (§3.3.10 requires equal types) |
| R5 | Wall times in a clock change: the skipped hour and the repeated hour | rec and du: skipped shifted, repeated first; Ical.Net differs | Skipped: shifted by the offset before the gap (RFC 5545 §3.3.5), or dropped with SkippedTimePolicy.Skip. Repeated: the first. | AmbiguousSkippedLocalTime, AmbiguousRepeatedLocalTime | 0 / 0 / 57 (rec and du give our list on all 85 components that hit a clock change; ical on 26 of 83) | 2 of 3 |
| R6 | EXDATE as a DATE on a date-time series | not in the corpus | The whole day is removed. | AmbiguousExdateType | 0 / 0 / 0 (unit test only) | prov. (clients treat a date as a day) |
| R7 | WEEKLY with BYSETPOS in the first, partial week | rec and du position inside the part of the week after DTSTART | Positions count in the whole week (§3.3.10), so an earlier instant of the first week takes a position and is then dropped as before DTSTART. | none | 1 / 1 / 0 | prov. (RFC) |
| Z1 | A TZID named both by a VTIMEZONE in the file and by the runtime's database | not compared (the oracles resolve zones in their own way; the zone fixtures use one or the other) | The VTIMEZONE wins. | none | n/a | prov. |
| Z2 | A vendor-prefixed TZID (/softwarestudio.org/Olson_20011030_5/America/New_York) | not compared | The longest tail of up to three segments that names a zone. | AmbiguousZoneName | n/a | prov. |
| Z3 | A Windows zone name with no VTIMEZONE | not compared; instants checked for 5 names (W. Europe, Eastern, AUS Eastern, New Zealand, Pacific) | The CLDR default-territory IANA zone. | AmbiguousWindowsZone | n/a | prov. (CLDR) |
| Z4 | A TZID that resolves nowhere | not compared | The values stay floating; no instant is claimed. | UnknownTimeZone | n/a | prov. |
| Z5 | An IANA TZID on a host whose runtime knows only Windows zone ids (Windows without ICU, or with InvariantGlobalization) | not compared | The runtime lookup is repeated under the Windows zone CLDR assigns the IANA name (default-territory row first, then any territory). The zone keeps its IANA Id and ZoneSource.Iana. Windows zone data has fewer historic rules than IANA (US dates before 2007 follow the current rules), so such instants can differ from the same file read on Linux or macOS | none | n/a | our default (CLDR) |
Limits of one oracle (not ambiguities; no entry in the resolution column):
| ID | Oracle | Behaviour | Differences |
| L-oracle-ignores-exrule | rec, du | EXRULE is not applied | 1 / 1 / n/a |
| L-du-glue-has-no-overrides | du | our glue expands the master only, so RECURRENCE-ID components are not applied | 0 / 3 / n/a |
| L-ical-duplicate-instants | ical | an instant produced by several BYxxx combinations is returned several times; the set of instants is ours | n/a / n/a / 10 |
| L-ical-window-wall-clock | ical | the window start is compared with the wall time of a zoned instant, so in zones east of UTC an instant before the window gets through; without those instants the lists are equal | n/a / n/a / 102 |
| L-ical-weekno-interval | ical | YEARLY with BYWEEKNO and INTERVAL above 1 lands off the interval grid (2026, 2029, 2031 for INTERVAL=2) | n/a / n/a / 8 |
| L-outvoted-by-the-other-two | any | the oracle differs and at least one other oracle gives our list | 1 / 0 / 77 |
The AmbiguousUntilType and AmbiguousExdateType resolutions are the ones most likely to change when Outlook is observed (the Outlook oracle, before Outlook writing). Counts come from one run of the 2,384 components; tools/calendar/run_expand_diff.sh repeats it. Zone behaviour is checked in the DST subset instead: 108 dst.ics components in America/New_York, Europe/Berlin, Australia/Sydney and Pacific/Auckland (both transitions each) and 9 with Windows names give our list on 108/108 and 9/9 for rec and du.
Mailbox files (mbox, Maildir, EMLX)
Black-box runs of Python 3.13.17 mailbox (mbox and Maildir) and MimeKit 4.18.2's mbox mode (MimeParser with MimeFormat.Mbox;
tools/oracles/mailbox-driver) over small probes (tools/mailbox/gen_mboxes.py, gen_stores.py) and the dovecot-crlf file
(3,240 From lines). Go has no mbox reader. Neither oracle is a mail client, so every row that is not "agree" is provisional.
The reader's behaviour is in mailboxes.md. MK = MimeKit, Py = Python.
| ID | Case | Py | MK | Resolution | Defect | Basis |
| S1 | A line starting From that is not an envelope line (From the desk of x, From maildir-storage.c, line 325:), unquoted in the body | splits | splits, then often fails on the next "message" | Only a line with a sender and a date after From (RFC 4155) starts a message; the rest is text. MboxFromLineMode.Any splits like both oracles. dovecot-crlf has three such lines: Py 3,240 messages, OneMail 3,237, MK raises at message 556 | AmbiguousFromLine | prov.; real-world file |
| S2 | A Content-Length header (mboxcl/mboxcl2, or in any file) | ignored | ignored | Used when the file's variant is unknown, mboxcl or mboxcl2 and the length ends exactly at an envelope line or the end of the file (a blank line may sit between). Otherwise ignored. mboxo and mboxrd ignore it. Two different values: the first | ContentLengthMismatch, AmbiguousContentLength | prov. |
| S3 | A CRLF file: the empty line after a message | keeps the CR LF in the message | the message runs to the next envelope line | The empty line (LF or CRLF) before the next envelope line or the end of the file is the separator, not message text | none | prov. |
| S4 | A body that the oracle's split leaves unparseable | n/a | raises FormatException | The tree is OneMail's job, not the splitter's: a message is never rejected | none | oracle limit |
| S5 | A line that is only From (no sender or date) | splits | does not split | Not an envelope line in strict mode; a delimiter in Any mode | AmbiguousFromLine | 1 of 2 |
| S6 | mboxcl2 (unquoted bodies, Content-Length only) | splits inside messages that hold envelope-looking lines | same | Read correctly through Content-Length; OneMail writes it only when asked for the variant | none | oracle limit |
| S7 | Bytes before the first envelope line | skipped | skipped | Kept as the store's preamble, so entries plus preamble tile the file | PreambleBeforeFirstMessage | all agree |
| S8 | An envelope line with no empty line before it | splits | splits | Splits; the previous message keeps its last line | MissingSeparatorLine | all agree |
| S9 | Unquoting a stored body when the variant is not known | n/a | n/a | mboxrd's rule (every >+From loses one >); an entry that a Content-Length delimited is returned as stored, because mboxcl and mboxcl2 differ | none | our default |
| S10 | Maildir info flags not in ASCII order or repeated (:2,SF) | read as written | n/a | Read as written, reported; written back sorted | UnsortedFlags | our default |
| S11 | Maildir file in new | subdir new, no flags | n/a | Flags from the name, plus Recent | none | all agree |
| S12 | Mail's flags word and date-sent/date-received reals in EMLX plists | n/a | n/a | Bits 0, 1, 2, 4, 6, 8 map to seen, deleted, answered, flagged, draft, forwarded; the raw word is always kept. A real is Unix seconds from 1e9, otherwise seconds since 2001-01-01. No real Mail store has been inspected, so this is a default to check against one | none | our default |
| S13 | Courier courierimapkeywords/:list | n/a | n/a | Keyword names, an empty line, then unique:n n rows with zero-based positions. Written from Courier's published description of the directory; the line format inside the files is not in that document, so this is unchecked against Courier | MalformedControlFile | our default |
| S14 | DBX: the length a message record states (field 0x11) differs from the chain's | n/a | n/a | undbx 0.21 writes the chain's bytes whatever the record states (3 records: stated 10 vs 3,024 bytes, 99,999 vs 63, 0 vs 723). The chain wins; the notes warn the field "can be wrong" | AmbiguousDbxDeclaredLength | undbx 0.21 on generated stores, and on a real store (NIST CFReDS Hacking Case): 33 of the 34 news posts that have text state a length 5 to 380 bytes different from the chain's, and undbx's file equals the chain in all 34 |
| S15 | DBX: the character set of record strings (subject, sender, folder names) | n/a | n/a | windows-1252. The notes name none; the strings are in the system ANSI code page of the machine that wrote them, which the file does not record | none | our default; not observable through undbx |
| S16 | DBX: a message file in the store folder that Folders.dbx does not name | n/a | n/a | Read, as a folder named after the file | DbxUnlistedFile | undbx 0.21 also extracts every file in the directory; our default for the folder name |
| S17 | DBX: a message record with no text chain (a header that was never downloaded) | n/a | n/a | An empty message in the folder, flagged. undbx 0.21 writes a 0-byte file for it (its -i option skips empty messages) | AmbiguousDbxNoMessageText | undbx 0.21 on a generated store; the real store above has 2,019 header-only records among 2,053 (news headers), and undbx writes each as a 0-byte file |
| S18 | DBX: field 0x09 of a folder record | n/a | n/a | Read as the special-folder kind (1 Inbox, 2 Outbox, 3 Sent, 4 Deleted, 5 Drafts). The notes describe the field twice (a per-folder index and the kind) | none | a real Folders.dbx (35,527 records) has the kinds on Inbox 1, Outbox 2, Sent Items 3, Deleted Items 4, Drafts 5 and absent on the rest |
| S19 | The character before the Maildir info section (unique:2,FS or unique;2,FS) | Py mailbox writes and reads : only | n/a | Read: the first :, else the first ; that is followed by 2,. Written: MaildirStoreOptions.InfoSeparator, default ; on Windows (where a file name cannot hold :) and : elsewhere; isync's InfoDelimiter documents the same default. No Windows mail client has been observed | none | our default (isync documentation) |
Reports, authentication results and MHTML
Probes are testdata/reports/… (cases.tsv describes each); MK is MimeKit 4.18.2's public API, Py is Python 3.13 email
(plus urllib.parse.urljoin for MH6–MH8). Go has no report or MHTML support and is not compared. All entries are provisional
(no client observed). Method: reports-and-mhtml.md.
| ID | Probe | MK | Py | Resolution | Defect | Basis |
| RP1 | dsn-11, dsn-12, dsn-32 extra blank lines between or around blocks | skipped | each is an empty block | Runs of blank lines are skipped; one defect per run. | AmbiguousReportBlankLines | prov. (MK, RFC) |
| RP2 | MDN/ARF body with a leading blank line, or several blocks | empty first block | empty first block | The first block is empty; further blocks are kept as extra blocks. | AmbiguousReportExtraBlocks | MK and Py agree |
| RP3 | dsn-13 a line without a colon in a block | kept as a field named by the line | block ends there | Skipped, reported. | ReportMalformedLine | prov. (no two agree) |
| RP4 | dsn-34 a name with a space | kept as a field | block ends there | Kept under its text before the colon. | InvalidHeaderName | prov. |
| RP5 | dsn-18 whitespace before the colon | trimmed | field lost | Trimmed, reported. | WhitespaceBeforeHeaderColon | prov. (MK, RFC 5322 obs-) |
| RP6 | dsn-14 a fold before any field | ignored | n/a | Skipped, reported. | ReportMalformedLine | prov. |
| RP7 | dsn-26 a lone CR in a value | kept | line split at CR | Stays data. | BareCarriageReturnInHeader | MK, same as the header reader |
| RP8 | dsn-21 an empty delivery-status body | no report | one empty block | A report with no groups. | ReportMissingGroup | prov. |
| RP9 | a whitespace-only line inside a block | fold | fold | A fold, not a block end. | none | all agree |
| RP10 | repeated single-use field (dsn-15 Action twice) | both kept in order | n/a | Both stay in the block; the typed accessor reads the first. | AmbiguousReportDuplicateField | prov. (lossless) |
| AR1 | auth-26, auth-08 first token is name=… (no authserv-id) | no id | n/a | No authserv-id; the token starts the first result. | AuthenticationResultsMissingId | MK |
| AR2 | auth-08 a property with no ptype (action=none) | dropped | n/a | Kept with an empty ptype. | AmbiguousAuthenticationResultsProperty | prov. (lossless) |
| AR3 | auth-11,12,14,15,16,18,19,27 broken fields (bad version, empty clause, garbage, none beside results, unterminated quote or comment) | the whole field fails | n/a | The clauses that can be read are kept, each repair is a defect. | AuthenticationResultsInvalidVersion, InvalidAuthenticationResults, AmbiguousAuthenticationResultsNone, UnterminatedQuote | lossless |
| AR4 | auth-10 quoted authserv-id, property value or reason (valid per RFC 8601 §2.2) | the field fails | n/a | Parsed. | none | RFC |
| RS1 | spf-06,07,10 unknown result, words that are not pairs, bad client-ip | no oracle | no oracle | Unknown result kept as SpfResult.Unknown with the text; bad pairs skipped. | InvalidReceivedSpf | prov. (RFC 7208 §9.1 only) |
| MH1 | mht-01 cid: references, with brackets, %40, or case | no match | n/a (no MHTML) | Matches Content-ID only, brackets stripped, percent-decoded, exact then ASCII case-insensitive. | none | RFC 2557 §8.3 |
| MH2 | mht-02,06,08,09,10,11,15,19 base from the part's or an enclosing entity's Content-Location | base only from the multipart's Content-Base | n/a | Base order of RFC 2557 §5 as written in the method note. | none | RFC |
| MH3 | mht-03 dot segments, no base | no match | n/a | RFC 3986 §5.2.4 on thismessage:/. | none | RFC |
| MH4 | mht-07 percent escapes, space, case of %hex | exact only | n/a | Exact first, then normalised. | none | prov. |
| MH5 | mht-17 mid: | no match | n/a | Needs a matching Message-ID and Content-ID. | none | RFC 2392 |
| MH6 | urljoin with base thismessage:/… | n/a | returns the reference unchanged (scheme not hierarchical for it) | Resolved per RFC 3986. | none | RFC |
| MH7 | urljoin of http:g against http: base | n/a | non-strict: http://a/b/c/g | Strict: http:g (RFC 3986 §5.2.2). | none | RFC |
| MH8 | urljoin with an upper-case scheme or host in the reference | n/a | lower-cases the scheme | Left as written; Normalize lower-cases when matching. | none | lossless |
| MH9 | two parts with the same location or content id (mht-06) | first | n/a | First wins. | AmbiguousMhtmlDuplicateLocation, AmbiguousMhtmlDuplicateContentId | prov. |
| MH10 | start names no part (mht-05) | n/a | n/a | The first part is the root. | MhtmlStartNotFound | RFC 2387 |
Documented oracle limits (not ambiguities; in explained.tsv as LIMIT-*): Python does not read
message/global-delivery-status or transfer-encoded report parts as reports.
The shared read view (bodies and attachments)
Checked against Python 3.13 email with policy.default (get_body/iter_attachments) over the probes, the Apache James test
messages, the dovecot mbox and a sample of the CMU Enron maildir; tools/oracles/viewcheck.sh reproduces the run. The view's
choices below are provisional: no mail client has been observed for them yet. The view raises no defects of its own; it
chooses between readings of a valid tree. The tree always keeps every part (docs/shared-view.md).
| ID | Case | Python email | Resolution |
| V1 | Which parts are attachments | Immediate children of the top multipart, except the first text/plain, text/html, multipart/related and multipart/alternative; a nested multipart is one attachment; a message that is not multipart has none | A leaf part or message/* part that is not the chosen text or HTML body, found by walking every multipart, in document order. A message that is a single leaf part (not text-body) is its own attachment. |
| V2 | Body inside multipart/signed or a nested multipart/mixed | get_body does not look inside multipart/signed; the nested part's text is listed as an attachment | The body is searched in the signed content (the first child) and in nested mixed parts, in order; the signature part is an attachment. |
| V3 | Body in multipart/alternative with several text parts | The last alternative that matches | The last alternative that matches (RFC 2046 5.1.4). The other text renderings are not attachments unless marked Content-Disposition: attachment. |
| V4 | Inline parts | Parts of a multipart/related are not listed | Listed, with IsInline set when the part is not marked attachment and has a Content-ID, so callers can resolve cid: references. |
| V5 | A text part with Content-Disposition: attachment | An attachment | An attachment; never the body. A text part with a file name but no disposition is still a body when it is the first match (the same as Python). |
| V6 | Text body without a usable charset | us-ascii, bytes above 127 replaced | UTF-8 when the bytes are valid UTF-8, otherwise Windows-1252 (N5's rule). |
| V7 | Address groups in From/To/… | Group name kept | Flattened to the members, the group name dropped (A1). The tree keeps the group. |
| V8 | Attachment file name | get_filename(): disposition filename, else type name, with the parameter rules of N1–N8 | The same order (N9). Names are kept as written, not made safe for a file system (N10); no name is null, never an empty string. |
| V9 | A message with a TNEF part (winmail.dat) | Python: winmail.dat is one attachment. MimeKit (black box, MimeKitTnef): MimeMessage.Attachments lists winmail.dat; TnefPart.ExtractAttachments returns the body (an unnamed RTF, HTML or text entry) and the attachments | A part that qualifies under MS-OXCMAIL 2.2.3.8 (the conversion's rule, conversion.md) is not listed; the attachments of its TNEF take its place in document order, RtfBody and the bodies the MIME side lacks come from it, and the part stays reachable (MimeMailItem.TnefParts, the tree). A part that does not qualify stays an attachment. Outlook shows a TNEF message this way (2.2.3.8); the attachments equal MimeKit's extraction, tnefparse's, tnef's and ytnef's on 64 wrapped messages (252 checks, 0 unexplained). Prov.: no client observed. |
MAPI and MSG
Observed on the Apache Tika and POI MSG files (55) and our generated ones, against extract-msg 0.55.0 as a black-box oracle.
No Outlook has been observed yet, so each resolution is provisional.
| ID | Case | Other reader | Resolution | Defect | Basis |
| X1 | Same property tag twice in one property stream | n/a | Both kept in the bag; lookups return the last | AmbiguousDuplicateProperty | prov. |
| X2 | Value stream with no property entry (POI fuzz-minimised file) | ignores it | Read as a property; the stream is the data | AmbiguousOrphanValueStream | prov., lossless |
| X3 | String type differs from the store support mask | follows the entry's type | The entry's own type decides | AmbiguousStringType | MS-OXMSG 2.4.2 |
| X4 | 8-bit strings, no PidTagMessageCodepage | fixed Latin default | Locale's ANSI page, then Internet code page, then enclosing message, then 1252 | AmbiguousCodePage | 36 corpus strings differ, all in files with only a locale or Internet code page |
| X5 | String size field: stream length or stream plus terminator | n/a | Both accepted | none | ANSI files store the NUL in the stream |
PST/OST messaging and LTP
Observed on the Tika testPST files, the CFReDS OST and our generated files against pffexport 20180714 as a black-box oracle. No Outlook
has been observed yet, so each resolution is provisional. L-entries are the lists-tables-properties choices of docs/pst-ltp.md.
| ID | Case | Other reader | Resolution | Defect | Basis |
| P1 | Message has attachment subnodes but no attachment table | no attachments | The attachment subnodes found are used | AmbiguousAttachmentTableMissing | prov., lossless |
| P2 | Embedded-message object property holds a bare subnode NID, not the (NID, size) structure | n/a | The NID is used | AmbiguousObjectReference | MS-PST 2.3.3.5 states the structure; prov. |
| P3 | Table row identifier differs from the NID the object is stored under | trusts the node | The node decides; the mismatch is reported | RowIdMismatch | prov. |
| L1 | Rows per block of a subnode row matrix | n/a | Taken from the first block's own size, which equals the formula for 8 KiB blocks | none | prov.; real files agree |
| L2 | BTH key that does not increase | n/a | Skipped, so a node reached twice gives no duplicates | none | prov. |
| L3 | Property or cell of an unknown type | n/a | Read as an HNID in a PC; kept inline when one, two or eight bytes | none | prov. |
| L4 | Positions in a heap's fill-level bitmap blocks (HNBITMAPHDR) | n/a | Allocations may not start before the block's header (12, 66 or 136... bytes, 2 elsewhere) | none | exercised on our files only |
RTF bodies (compressed RTF, de-encapsulation)
Probes are the 45 RTF bodies of the public Tika and POI .msg files, the specification examples (testdata/rtf), and 3,000
damaged streams. Oracles: compressed-rtf 1.0.7 (decompress, compress) and RTFDE 0.1.2.2 (de-encapsulation), run as released
packages; the message's own PidTagHtml/PidTagBody is a third reference. Where the specification is explicit the entry says so.
Entries without an observed Outlook-side result are provisional. Method: rtf.md.
| ID | Case | Oracle / own body | Resolution | Defect | Basis |
| RT1 | \par and \line in the extracted text | RTFDE writes LF; own plain-text bodies use CRLF (4 of 4) | CRLF (MS-OXRTFEX 2.2.3.2 and 2.2.3.3) | none | spec; seen in 31 HTML and 4 text results vs RTFDE |
| RT2 | A literal no-break space (\'a0) in visible text | RTFDE writes | The character U+00A0. A string has no code page to lose it in | none | prov.; 4 files vs RTFDE |
| RT3 | The message's own PidTagHtml carries <meta http-equiv=Content-Type …> that the RTF's HTML lacks | own body | Not part of the encapsulated HTML; nothing to resolve. Callers who want the stored body read PidTagHtml | none | 5 of 6 own HTML bodies |
| RT4 | Own PidTagHtml is a re-serialised copy of the RTF's HTML (tag case, dropped </tbody>) | own body | The encapsulated HTML is returned as written; same text (checked by words) | none | 1 of 6 (bug66335); RTFDE agrees with OneMail on it |
| RT5 | \_ | spec table: soft hyphen in \htmltag; RTF 1.9.1: non-breaking hyphen | U+00AD inside \htmltag, U+2011 elsewhere | none | spec |
| RT6 | \htmlrtf active when an \htmltag group starts; code page of \htmltag text | spec: content MUST be copied; default code page MUST be used | Copied whatever the state; decoded with the document's default code page, not the font's | none | spec |
| RT7 | \uN and \lquote, \~… inside \htmltag | spec table gives HTML text (€, ‘); 2.2.3.2 says "Unicode characters" | The characters. A .NET string needs no entities, and both denote the same text | none | prov. |
| RT8 | How many characters \ucN skips | RTF 1.9.1 says "characters", with \'hh as one | Text bytes and \'hh escapes count; control words do not; { or } cancels the rest; \uc is group-scoped | none | prov. |
| RT9 | Code page of text bytes | spec: "the code page of the current font" | Font \cpgN, else \fcharsetN (0 = 1252 even under another \ansicpg; 1 = document), else \ansicpg/\ansi/\mac/\pc/\pca, else 1252; before any \f the \deff font | none | spec example uses \fcharset0 under \ansicpg1251 for exactly this; the corpus Cyrillic body decodes to its own body |
| RT10 | \htmlrtf in a \fromtext document | spec defines it for HTML only | Ignored | AmbiguousHtmlRtfInPlainText | prov. |
| RT11 | A code page with no decoder (symbol, platform gap) | spec silent | The document's default code page | AmbiguousUnknownCodePage | prov. |
| RT12 | Detection: \fromhtml/\fromhtml0, text before the control word, an eleventh token, CR/LF before the first token | spec 2.2.3.1 (SHOULD) | Only \fromhtml1 or \fromtext within ten group-start/control-word tokens after {\rtf; line breaks are skipped as in RTF | NotEncapsulated, NotRtf | spec |
| RT13 | Destinations | spec: skip standard destinations without visible text, keep \fonttbl; ignorable \* groups other than \htmltag skipped | A fixed list of body-less destinations is skipped; an unknown destination that is not ignorable is read as text (it cannot be told from a formatting group) | none | prov. |
| RT14 | RAWSIZE far above the data (a decompression bomb) | compressed-rtf decodes the data and ignores the claim | Refused before allocating: above 256 MiB, or above 20 × the input length | OutputLimitExceeded, ExpansionRatioExceeded | 123 of 3,000 damaged streams differ for this reason only |
| RT15 | COMPSIZE disagrees with the bytes present | compressed-rtf ignores it | Contents are bytes 16 to COMPSIZE + 4 when that is within the input, else to the end; the CRC covers exactly those | CompressedSizeMismatch | spec 2.1.3.1.1; prov. for the extra bytes |
| RT16 | Data ends before the end marker | compressed-rtf returns what it decoded | The decoded bytes, with a defect | MissingEndMarker | prov. |
| RT17 | RAWSIZE disagrees with the output; MELA contents | spec: MELA CRC not validated, size may be read either way | Output is what the data holds (growing up to the limit), with a defect; MELA is the contents as stored | RawSizeMismatch | spec 2.2.3.1 |
| RT18 | Compressing nothing | spec note: Outlook writes 02 00 0D 00 | 01 0C F0 (end marker only) is written; 02 00 0D 00 is read as one NUL byte | none | spec |
TNEF
Observed on the public Apache POI and Tika TNEF files (6 distinct) and our generated ones, against tnefparse 1.4.0, tnef 1.4.18 and ytnef 2.1.2
as black-box oracles. No Outlook has been observed, so each resolution is provisional unless it follows the specification's own text.
| ID | Case | Other reader | Resolution | Defect | Basis |
| T1 | A message-level legacy attribute appears twice | n/a | The last one is used; both stay in the attribute layer | AmbiguousDuplicateAttribute | prov., lossless |
| T2 | A legacy attribute and the encapsulated property differ (e.g. attDateRecd vs PidTagMessageDeliveryTime in winmail-sample1.dat) | n/a | The encapsulated property wins; the legacy value is not added | AmbiguousLegacyConflict | MS-OXTNEF 2.1.3; seen in 1 corpus file |
| T3 | PtypNull value in a property list | tnef and ytnef reject it (0 and 4 bytes alike) | Takes no bytes | none | MS-OXCDATA 2.11.1; prov. |
| T4 | Legacy message class names (IPM.Microsoft Mail.Note) | n/a | Mapped to the modern class | AmbiguousMessageClass | MS-OXTNEF 2.3.3.4; the spec sample and corpus files use it |
| T5 | Wrong checksum on the message class attribute | n/a | Ignored, the data is used | AmbiguousMessageClassChecksum | MS-OXTNEF 2.3.3.4 |
| T6 | An embedded-message or OLE attachment also has attAttachData | n/a | The attribute is ignored | AmbiguousAttachDataIgnored | MS-OXTNEF 2.3.3.7 |
| T7 | attOemCodepage differs from PidTagMessageCodepage | n/a | attOemCodepage decides how 8-bit strings are read | AmbiguousCodePage | MS-OXTNEF 2.3.3.2 order; no corpus file has the conflict, so no client observation |
| T8 | attOwner with no meeting class | n/a | Maps to the PidTagSentRepresenting group | AmbiguousOwner | prov. |
MS-OXCMAIL conversion (MAPI to MIME and back)
The rules are in conversion.md. No Outlook has been observed yet; the two oracles are black-box MimeKit 4.18.2 and
Python 3.13 reading what we write, and the corpus is the Tika and POI MSG files and the Tika and CFReDS PST/OST files. Each
resolution is provisional until Outlook can be run.
| ID | Case | What the spec says | Resolution | Defect or report entry | Basis |
| OX1 | An EX sender or recipient has no SMTP address property | 2.1.3.1.7: look it up in the address book directory | Without a directory: the same distinguished name elsewhere in the message, then the matching transport From/Sender field (one mailbox), then a unique display name in the transport headers. Never derived from the distinguished name | AddressUnresolved when nothing resolves | on the 45 MSG and 25 PST/OST items, 16 of 28 EX senders and 50 of 50 EX recipients resolve; the 12 senders that do not (all MSG) have no SMTP property and no transport From field; 9 PST senders have no address at all |
| OX2 | Transport headers and properties disagree | 2.2.3.2.28 keeps the received block; 2.1.3.2 builds fields from properties | The received field stays when it agrees; the properties win when it does not (a user edit makes the properties newer) | TransportHeaderOverridden, NamedPropertyValueSetAside | 2 fields and 5 named-property values across the 70 corpus items |
| OX3 | Default values (PidTagSensitivity 0, false receipt flags) | 2.1.3.2.6 and 2.1.3.1.5: no field | No field; reading back leaves the property absent | DefaultValueNotWritten | 2.1.3.2.6, 2.1.3.1.5; 151 entries over the 70 exported items |
| OX4 | Importance: Normal | 2.1.3.2.5: MAY be omitted | Written whenever the property exists (0, 1, 2) | none | 2.1.3.2.5 (MAY omit); writing it keeps the property on the way back, 0 silent differences in the 70-item property round trip |
| OX5 | RTF that encapsulates nothing | 2.1.3.3: not covered | A text/rtf part when there is no HTML, else not written | RtfCarriedAsTextRtf, RtfRedundant | 2.1.3.3 is silent; 5 and 13 entries over the 70 exported items; no client observed |
| OX6 | Case of a PS_INTERNET_HEADERS name | 2.2.3.2.29: "the header name" | As the message wrote it. Stores differ (Exchange stores lower case, Outlook as written), so comparisons are case-insensitive | none | 2.2.3.2.29; the MSG and PST corpora hold both spellings, so the property round trip compares case-insensitively |
| OX7 | Missing or invalid Date on import | 2.2.3.2.2: the current time | No PidTagClientSubmitTime; a conversion must be reproducible. Export falls back to the delivery time | DateFromDeliveryTime | deviates from 2.2.3.2.2 on purpose (reproducible output); 4 MSG items and 1 Mime4j message hit it |
| OX8 | Non-ASCII display name | 2.1.3.1: RFC 2047 | The whole name as one run of encoded words (never leaving "@" or "(" in plain text beside them) | none | MimeKit and Python read it; a plain-text remainder broke an address in a first attempt |
| OX9 | Which MIME part is the body | 2.2.3.3.2 | The part the shared view chooses (shared-view.md), a named text part included | none | 2.2.3.3.2; body hashes equal the view's on 70 of 70 exported items in both oracles |
| OX10 | Composite media type on flat attachment data | not covered | application/octet-stream | CompositeTypeOnFlatData | spec silent; a multipart/signed without a boundary broke the parse in MimeKit and Python; 1 MSG item and 6 Mime4j messages hit it |
| OX11 | Bcc | 2.1.3.1.1.2 | Not written unless asked; a Bcc field on import is Bcc recipients | BccNotWritten | 2.1.3.1.1.2; 2 MSG items, 2 Mime4j and 1,272 Enron-sample messages hit it |
| OX12 | multipart/signed on import | 2.2.3.4 | Class IPM.Note.SMIME.MultipartSigned; content and signature become a body and an attachment | SignatureNotVerifiable | 2.2.3.4; 213 of 1,430 Dovecot messages; the Outlook form, while signed MIME stays bytes elsewhere |
| OX13 | Subject prefix | 2.2.3.2.6.1: one to three characters other than colon, blank or digit, then colon and blanks | As stated, with an empty PidTagSubjectPrefix when there is none | none | 2.2.3.2.6.1; 23 PST items carry a prefix marker the export removes (SubjectPrefixMarkerRemoved), none differ in the oracles |
| OX14 | Embedded message names | not covered | The attachment's own name only; the subject is not invented as a name | none | spec silent; attachment names of all 70 exported items equal the source in both oracles |
| OX15 | Reply-To entries | 2.1.3.1.2: names ignored when the counts differ | Only one-off SMTP entries are written; address-book entries need a directory | ReplyToUnresolved | 2.1.3.1.2; 9 entries over the 70 exported items |
| OX16 | A TNEF property that a header field also sets (subject, sender, reply-to) | 2.2.3.8 step 2.1: ignore attributes that set a property already set by the headers | Per property, and a group set by any header (subject, sender, sent-representing sender, reply-to) is the headers' whole, so two sources are never mixed in one sender or subject. Ignored values that differ are reported | TnefPropertyIgnored | 2.2.3.8; 152 of 1,596 properties over 52 wrapped corpus messages (none in the bare shape, which has no headers); prov. for groups |
| OX17 | Plain text in MIME, richer body in the TNEF | 2.2: the plain text MUST NOT be preferred to a richer TNEF body | A body property is added when the MIME side has none of that kind; the MIME text stays PidTagBody; PidTagNativeBody and PidTagInternetCodepage are the TNEF's (the author's) unless the HTML is the MIME side's, then the richest body | TnefPropertyIgnored for a differing body of the same kind | 2.2; prov. for the codepage rule |
| OX18 | Message class | the class is a property; header processing gives IPM.Note | The default IPM.Note gives way to the TNEF's IPM.* class; a class the MIME structure gave (report, S/MIME) stays | TnefMessageClassUsed | spec silent on defaults; needed for meeting requests carried in TNEF; prov. |
| OX19 | Recipients in the headers and in the TNEF | 2.2.3.8 step 2.1 | The headers' recipients win; the TNEF table is used only when the headers gave none | TnefRecipientIgnored | 2.2.3.8; prov. |
| OX20 | A TNEF stream ending in CR LF | 2.2.3.8 step 3: SHOULD ignore the stream (a SHOULD, no client observed) | Unpacked, as tnef 1.4.18, ytnef 2.1.2, tnefparse 1.4.0 and MimeKit 4.18.2 all do (4 of 4 oracles on both bug52400 files); reported, not hidden. Prov. until a client is observed | AmbiguousTnefTrailingCrLf (view defect), TnefTrailingCrLfIgnored (Internal loss entry) | 2.2.3.8 step 3; the two bug52400 corpus files; the earlier choice (keep as attachment) was reversed on the oracle evidence |
| OX21 | Correlation key vs X-MS-TNEF-Correlator | 2.2.3.8 step 4: a mismatch means pure MIME; the field is spelled X-MS-TNEF-Correlator (2.1.3.2.26) and X-MS-Tnef-Correlation (2.2.3.8) | Both spellings are read; the binary key matches when its bytes (trailing NULs and blanks removed) equal the field as ASCII text or as hexadecimal digits; either one missing is a match | TnefNotUsed | 2.1.3.2.26, 2.2.3.8; none of the corpus files has a key; prov. |
| OX22 | Writing a TNEF message | 2.1.3.7; the key is "unique" | The key of the message's own PidTagTnefCorrelationKey when it has one, else the first 16 bytes of SHA-256 of Message-ID (or subject) and submit time in hex; no top-level recipient table in the TNEF; an empty text/plain when there is no text body | TnefWriterLimit, TnefPlainTextNotRendered | 2.1.3.7, 2.1.3.1.1; 45 MSG and 25 PST/OST corpus items through OxcMailTool rt-tnef: 10,496 of 11,142 properties exact, 227 the same text under another string type, the rest in the loss report, 0 silent (pure MIME: 6,453 exact, 4,290 reported) |