Skip to content

Conversation

@nevans
Copy link
Collaborator

@nevans nevans commented Feb 10, 2026

resp-text is defined as:

resp-text = ["[" resp-text-code "]" SP] text     ; RFC3501
resp-text = ["[" resp-text-code "]" SP] [text]   ; RFC9051

And our even more lenient interpretation (which incorporates RFC9051 errata), is this:

resp-text = ["[" resp-text-code "]" [SP [text]] / [text]

And, although the resp-text-code grammar is elaborate, in both RFC3501 and RFC9051, the final alternative is a superset of every other alternative:

resp-text-code /= atom [SP 1*<any TEXT-CHAR except "]">]

Notice that text is a superset of atom, SP, "[", "]", and every other allowed character in resp-text-code. So, if resp-text-code fails, resp-text should backtrack and fallback to just parsing everything as text.

However, (prior to this PR) the parser commits to resp-text-code as soon as "[" is encountered at the beginning of resp-text. If what follows is not resp-text-code, maybe it doesn't begin with an atom or it doesn't have a closing "]", then we fail to parse resp-text correctly.

Fixing this requires either looking further ahead more than a single token or backtracking when resp-text-code fails to parse. In this case, I think backtracking is the best approach. We don't need to worry about backtracking taking exponential time, because the fallback (text) is exceptionally simple (does not call any other productions), will parse all the way up to the next CRLF, and so nothing backtrackable can be nested.

This uses ResponseParser#current_state that was added for #599 and relies on #600 to ensure that debug error messages are not printed when backtracking.

Fixes #597.

This can be used to catche parse errors, restore the parser state, and
then either call a fallback or re-raise the error.

NOTE: Reckless backtracking can lead to O(n**2) situations, so it
should very rarely be used.  Fallbacks should not backtrack.
`resp-text` is defined as:
```abnf
resp-text = ["[" resp-text-code "]" SP] text     ; RFC3501
resp-text = ["[" resp-text-code "]" SP] [text]   ; RFC9051
```
And our even more lenient interpretation (which incorporates RFC9051
errata), is this:
```abnf
resp-text = ["[" resp-text-code "]" [SP [text]] / [text]
```

And, although the `resp-text-code` grammar is elaborate, the final
alternative is a superset of every other alternative (in both RFCs):
```abnf
resp-text-code /= atom [SP 1*<any TEXT-CHAR except "]">]
```

Notice that `text` is a superset of `atom`, `SP`, `"["`, `"]"`, and
every other allowed character in `resp-text-code`.  So, even if
`resp-text-code` fails, `resp-text` may still succeed by parsing
everything as `text`.

However, (prior to this commit) the parser commits to `resp-text-code`
as soon as `"["` is encountered at the beginning of `resp-text`.  If
what follows is not `resp-text-code`, maybe it doesn't begin with an
`atom` or it doesn't have a closing `"]"`, then we fail to parse
`resp-text` correctly.

Fixing this requires either looking further ahead more than a single
token or backtracking when `resp-text-code` fails to parse.  In this
case, I think backtracking is the best approach.  We don't need to worry
about backtracking taking exponential time, because the fallback
(`text`) is exceptionally simple (does not call any other productions),
will parse all the way up to the next CRLF, and so nothing backtrackable
can be nested.

This uses `ResponseParser#current_state` that was added for #599 and
relies on #600 to ensure that debug error messages are not printed when
backtracking.

Fixes #597.
@nevans nevans force-pushed the parser/backtracking-in-resp-text branch from 675b663 to 7fb8b57 Compare February 10, 2026 22:07
@nevans nevans merged commit 1abcd97 into master Feb 10, 2026
39 checks passed
@nevans nevans deleted the parser/backtracking-in-resp-text branch February 10, 2026 22:11
@nevans
Copy link
Collaborator Author

nevans commented Feb 10, 2026

Note that this still preserves uid-set with * inside a response code as an error, for two reasons:

  • because that situation probably only occurs because of a malignant server (CVE-2025-25186)
  • but mostly because existing tests expect that, and I didn't want to update them for this PR.

@nevans
Copy link
Collaborator Author

nevans commented Feb 10, 2026

Another follow-up PR is still needed to handle resp-text-code failures, when it can be parsed as atom [SP 1*<any TEXT-CHAR except "]">], but parsing based on the name failed. After this PR, that situation no longer crashes, but it won't return the ResponseCode either. Arguably, that should return ResponseCode, but use UnparsedData for the ResponseCode#data (or replace UnparsedData with something new like InvalidParsedData).

On the other hand, this might be considered a breaking change: it would break code that expects ResponseCode#data to be successfully parsed based on what was found in ResponseCode#name, which is a reasonable expectation for the current version.

@nevans nevans added the bug Something isn't working label Feb 10, 2026
rescue ResponseParseError => error
raise if /\buid-set\b/i.match? error.message
restore_state state
text
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops. This is a bug. It should be ResponseText.new(nil, text? || "")

nevans added a commit that referenced this pull request Feb 11, 2026
This bug was introduced in #601, which hasn't been released yet.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sporadic Net::IMAP::ResponseParseError: unexpected QUOTED (expected "]") on Outlook365

1 participant