Skip to content

message: keep the header of a message that failed to parse - #468

Open
hilli wants to merge 1 commit into
mjl-:mainfrom
hilli:fix-fallback-header-offsets
Open

hilli wants to merge 1 commit into
mjl-:mainfrom
hilli:fix-fallback-header-offsets

Conversation

@hilli

@hilli hilli commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

A message whose parse fails before the end of the header is found is stored with
HeaderOffset == BodyOffset, so the whole message becomes its body. Over IMAP,
FETCH BODY[HEADER] and BODY[HEADER.FIELDS (...)] return a zero-length
literal, while BODY[TEXT] returns the header as well.

newPart sets both offsets to the start of the header and only moves
BodyOffset after reading the empty line that ends it:

p.HeaderOffset = b.offset
p.BodyOffset = b.offset
for {
	line, _, err := b.ReadLine(true)
	...
	if err != nil {
		return p, fmt.Errorf("reading header line: %w", err)  // both offsets still at the start
	}
}
p.BodyOffset = b.offset  // only on success

fallbackPart copies those offsets verbatim.

Reproducing

Append a message with one header line longer than maxLineLength and fetch it
back. Against mox localserve, which sets Pedantic: true and so uses the 1000
byte limit:

  line   990 bytes -> HEADER  1037  TEXT     7   ok
  line  1000 bytes -> HEADER  1048  TEXT     7   ok
  line  1001 bytes -> HEADER     0  TEXT  1056   <- header empty, body is everything
  line  9000 bytes -> HEADER     0  TEXT  9055   <-

With Pedantic: false the threshold moves to 8192, as maxLineLength() says,
and the behaviour above 8192 is the same. Same results on a clean checkout of
main and on a branch of my own, so it is not something local to me.

The APPEND is accepted, so the message is stored this way permanently.

Why such a message shows up

SMTP submission rejects it: 550 5.6.0 cannot parse header or From address: parsing message: reading header line: line too long. Inbound delivery logs the
parse error and continues (smtpserver/server.go, in deliver), and IMAP
APPEND has no such gate at all.

I ran into it migrating a mailbox into mox with an IMAP client: 19 messages out
of 11770 in a Sent mailbox, all filed there by Apple Mail, which appends its own
copy of a sent message and writes References unfolded. A long enough thread
gives a header line over 998 bytes. The messages are malformed, but mox has
decided to keep them rather than reject them, and MessageAdd notes "p is still
valid" where it stores the part.

The change

Look for the end of the header again in the fallback, without a limit on line
length. Only when parsing stopped before the split was established, so a part
whose header parsed and that failed later keeps the offsets it has; if there is
no empty line at all, nothing changes. Messages that parse today are unaffected,
since fallbackPart is only reached after an error.

Existing messages can be repaired with mox reparse.

The test covers the basic case and the empty line straddling two chunks of the
scan. go test ./message/ ./imapserver/ ./store/ ./smtpserver/ ./webmail/ passes.

Not addressed

The fallback still reports application/octet-stream with an empty envelope, so
ENVELOPE and BODYSTRUCTURE stay uninformative for these messages even though
the header is now readable. Parsing the recovered header to fill those in, or
skipping just the over-long field so the message parses normally, both seemed
like bigger decisions than this fix, and I would rather leave them to you.

newPart sets HeaderOffset and BodyOffset to the start of the header, and only
moves BodyOffset once it has read the empty line that ends the header. When
parsing stops before that, e.g. on a header line longer than maxLineLength,
both offsets are still at the start. fallbackPart copies them, so the part it
returns has a zero-length header and a body that is the whole message.

Over IMAP that means FETCH BODY[HEADER] and BODY[HEADER.FIELDS] return nothing,
while BODY[TEXT] returns the header as well. The bytes are not withheld, they
are served in the wrong place, and a client that reads headers to identify a
message sees none.

Messages like this arrive over IMAP APPEND, which is not gated by the line
length limits that SMTP submission applies. Apple Mail files its own copy of a
sent message into the Sent mailbox that way and writes References unfolded, so
a long enough thread gives a header line over 998 bytes. Such a message is
malformed, but mox has already decided to store it rather than reject it, and
MessageAdd notes that p is still valid where it does so.

So look for the end of the header again in the fallback, without a limit on the
length of a line. Only when parsing stopped before the split was established: a
part whose header parsed and that failed later keeps the offsets it has, and if
there is no empty line at all nothing changes.

Messages already stored with the old offsets can be repaired with "mox reparse".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant