fix: guard QuotedPrintable decode against truncated =XY escapes#283
Open
guettlibot wants to merge 1 commit into
Open
fix: guard QuotedPrintable decode against truncated =XY escapes#283guettlibot wants to merge 1 commit into
guettlibot wants to merge 1 commit into
Conversation
decodeText read cleaned.substring(i + 1, i + 3) unconditionally whenever it saw '='. Two paths could take i + 3 past cleaned.length and throw a RangeError: * the outer branch, when the input ended with a bare '=' or '=X' * the inner while loop, whose guard cleaned.length > (i + 4) proves cleaned[i + 3] exists but not that the substring(i+4, i+6) after i += 3 is in bounds RFC 2045 §6.7 permits consumers to preserve malformed input verbatim, which matches how the existing int.tryParse == null branch already handles non-hex '=XY'. Extend that to both truncated cases: preserve the tail verbatim on the outer branch and bail out of the inner loop so the outer branch can pick up the leftover characters. Reported from downstream in guettli/sharedinbox#232, where an incoming mail body whose quoted-printable payload ended mid-escape crashed the entire app via runZonedGuarded.
guettlibot
pushed a commit
to guettli/sharedinbox
that referenced
this pull request
Jul 11, 2026
The RangeError from QuotedPrintableMailCodec.decodeText that triggered the crash in #232 is fixed upstream in Enough-Software/enough_mail#283. Note the reference next to the defensive try/catch so a future reader knows the underlying bug is being tracked upstream and knows why the guard stays even after the dependency bumps: it still covers network/DB failures in the opportunistic prefetch path. Closes #235
This was referenced Jul 11, 2026
guettli
pushed a commit
to guettli/sharedinbox
that referenced
this pull request
Jul 16, 2026
* docs(di): cross-reference upstream enough_mail fix for #232 crash The RangeError from QuotedPrintableMailCodec.decodeText that triggered the crash in #232 is fixed upstream in Enough-Software/enough_mail#283. Note the reference next to the defensive try/catch so a future reader knows the underlying bug is being tracked upstream and knows why the guard stays even after the dependency bumps: it still covers network/DB failures in the opportunistic prefetch path. Closes #235 * test(fuzz): double-sync after JMAP mutations to unblock CI The long-term IMAP/JMAP fuzz test at step 6 (JMAP mutations) has been flaking on CI with sporadic 'Mismatch after JMAP mutations!' — EmailFieldMismatch.flagged on a random message, sometimes accompanied by missingInA/missingInB when the moveEmail hasn't landed on the IMAP side yet. Both this PR's initial run and the rerun hit it with different random IDs, and main is intermittently affected too. The single-round sync at line 369-370 assumes Stalwart bumps the IMAP HIGHESTMODSEQ on the same tick that the JMAP mutation lands. It doesn't always; CONDSTORE then skips the mailbox and the assertion fires. Wrap the pair in a 2-round loop so the second pass picks up the delayed HIGHESTMODSEQ bump, mirroring the same pattern already used elsewhere. This is a straight cherry-pick of the test-only change from the abandoned PR #204 / commit 8f0b834 ("fix(jmap/tests): retry transport errors and double-sync after JMAP mutations"). That PR was closed without merging so this fix never landed; the sibling jmap_client.dart / account_comparison_stalwart_test.dart pieces of 8f0b834 are deliberately not cherry-picked here to keep the scope of this docs-tracking PR minimal — they can land separately if the transport-retry problem is still occurring. Unblocks CI on #237. --------- Co-authored-by: AgentLoop <agentloop@thomas-guettler.de>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
QuotedPrintableMailCodec.decodeTextatlib/src/codecs/quoted_printable_mail_codec.dart:199callscleaned.substring(i + 1, i + 3)unconditionally whenever it encounters=. Two independent inputs makei + 3exceedcleaned.lengthand throw aRangeError:=or=X(e.g."hello=","hello=A","h=A").cleaned.length > (i + 4) && cleaned[i + 3] == '='provescleaned[i + 3]exists, but after the followingi += 3the substring readscleaned[i+4..i+5], so a valid multi-byte escape followed by a truncated tail like"hi=C3=B6=A"still throws.Minimal repro against 2.1.7 / current
main:Fix
RFC 2045 §6.7 permits consumers to preserve malformed quoted-printable input verbatim, and the existing
int.tryParse == nullbranch already does that for non-hex=XY. This PR extends that policy to the two truncated cases:substring, checki + 3 > cleaned.length; if so, append the malformed tail (cleaned.substring(i)) to the buffer and break.if (i + 6 > cleaned.length) break;so a truncated inner escape falls through to the outer truncated-tail branch on the next outer iteration.No behaviour change for well-formed input; the full existing test suite passes (
dart test, 420 tests).Tests
Added a regression group
malformed truncated =XY escapes are preserved verbatimintest/codecs/quoted_printable_mail_codec_test.dartcovering:==A=Aat end of a short input=A(exercises the inner-loop path)Each asserts the decoder returns the input verbatim rather than throwing.
Downstream context
Reported from downstream in guettli/sharedinbox#232, where an incoming mail body whose quoted-printable payload ended mid-escape crashed the entire Flutter app via
runZonedGuarded → FlutterError.onError → CrashScreen. That project has since added a defensivetry/catchat the call site, but the foregroundgetEmailBodypath for the currently viewed email still hits the same crash on any similar malformed message. This upstream fix protects all callers.Non-goals
int.parseinside the inner while loop (which can still throwFormatExceptionon non-hex input there). That is a distinct latent bug from BUG: in serialization of ServerConfig.fromJson() #232 and is left for a focused follow-up.print(...)diagnostics.