---
title: "Reports, MHTML and authentication results"
url: https://recursiveai.org/onemail/docs/reports-and-mhtml/
description: "Typed views over the MIME entity tree (RecursiveAI.OneMail.Mime.Reports, .Mhtml, .Authentication). The tree stays the lossless store: a view reads it…"
version: "0.3.0-preview.2"
source: https://github.com/recursive-ai-oss/onemail-csharp/blob/bb4c9543917ab4ad466b5200d132e1899cb3ca14/docs/reports-and-mhtml.md
---

# Reports, MHTML and authentication results

Typed views over the MIME entity tree (`RecursiveAI.OneMail.Mime.Reports`, `.Mhtml`, `.Authentication`). The tree stays the
lossless store: a view reads it, never changes it, so an untouched message still writes back byte for byte. Every repair is a
typed defect with the byte offset. The ambiguity entries (RP, AR, RS, MH) are in [ambiguities.md](https://recursiveai.org/onemail/docs/ambiguities/).

## multipart/report (RFC 6522)

`MimeReport.Find(tree)` returns the first `multipart/report` (searching nested message/rfc822 as well); `TryCreate(entity)`
tests one entity. The view exposes the human-readable part, the machine-readable part, and the original (a message, or
headers only for `text/rfc822-headers` and `message/rfc822-headers`). `GetDeliveryStatus`, `GetDispositionNotification` and
`GetFeedbackReport` parse the machine part and add their defects to a caller's list.

| Report | Media type | Spec | Class |
|---|---|---|---|
| Delivery status | `message/delivery-status`, `message/global-delivery-status` | RFC 3464, 6533 | `DeliveryStatusReport` (per-message block, per-recipient `RecipientStatus`) |
| Disposition notification | `message/disposition-notification` | RFC 8098 (3798) | `DispositionNotification`, `MdnDisposition` |
| Feedback | `message/feedback-report` | RFC 5965, 6591, 6650 | `FeedbackReport` |

- **Field syntax** (`ReportFieldParser`): RFC 5322 header syntax, a block ends at an empty line. A line of only blanks is a
  fold, not an empty line. Values are not RFC 2047-decoded (the RFCs do not use encoded words here); bytes are read as UTF-8
  if valid, else windows-1252. A lone CR stays in the value (`BareCarriageReturnInHeader`).
- **Typed values**: `DsnTypedValue` (`type; value`), `EnhancedStatusCode` (RFC 3463; trailing comments tolerated),
  `DsnAction`, dates as `MimeDate`. A value that does not fit stays readable as text and raises `ReportInvalidFieldValue`.
- **Kind**: the machine part's own media type decides; a `report-type` that disagrees or is missing raises
  `ReportStructureInvalid`.
- **Limits**: a report body over 16 MiB or more than 100,000 fields raises `ReportLimitExceeded`; the rest is not parsed.
- **Writing**: `MimeReport.CreateDeliveryStatus / CreateDispositionNotification / CreateFeedbackReport` build the
  `multipart/report` (boundary overridable for reproducible output); `ToBytes` writes fields folded at 78 columns, UTF-8 for
  non-ASCII (the caller picks the `global` media type with `RequiresGlobal`).

## MHTML (RFC 2557)

`MhtmlDocument.Find / TryCreate / Open / Load` view a `multipart/related` (or a lone HTML part) as a root resource plus
resources. `Resolve(reference, htmlBase)` returns the resource a reference in the HTML names; `ResolveToAbsolute` gives the
URI. `MhtmlBuilder` writes a `.mht`. `MhtmlUri` is RFC 3986 §5.2 resolution, with `thismessage:/` as the base of last resort.

Base order (RFC 2557 §5): the HTML's own base argument, the legacy `Content-Base`, an absolute `Content-Location` of the
part and then of its enclosing entities, else `thismessage:/`. Lookup is exact first, then after `Normalize` (scheme and host
case, percent-hex case, space as `%20`, default ports, dot segments). `cid:` matches `Content-ID` only (RFC 2557 §8.3: a
`Content-Location` holding a `cid:` URL never satisfies it), with angle brackets stripped and percent-decoding; `mid:` needs
both a content id and the message id. A lookup falls through to enclosing `multipart/related` entities. Duplicates: the first
wins, and the ambiguity is reported.

## Authentication-Results and Received-SPF

`MimeEntity.GetAuthenticationResults()` and `GetReceivedSpf()` parse every such field (RFC 8601; RFC 7208 §9.1) into
`AuthenticationResults` (authserv-id, version, `AuthenticationMethodResult` with method, version, result, comment, reason and
`AuthenticationProperty` ptype.name=value) and `ReceivedSpf`. This is parsing only: nothing is evaluated or trusted, and
callers must still check the authserv-id against their own boundary. `MimeEntity.GetAuthenticationResults` takes the header
text as the tree stores it (comments and folds kept in the raw value).

## Method

`tools/oracles/reports` runs ours, MimeKit 4.18.2 (public API) and Python 3.13 `email` over `testdata/reports` (119 messages,
generated by `tools/reports/make_fixtures.py`, with RFC 3464, 8601, 7208 and 2557 examples) and over the corpus messages that
carry the headers (`corpus.sh`). Python has no Authentication-Results parser; Received-SPF has no oracle (RFC examples only).
MHTML reference resolution is also run against Python `urllib.parse.urljoin` on 218 base/reference pairs. A difference must
match a line of `explained.tsv` (naming an ambiguity entry or a documented oracle limit) or the run fails.
