Skip to content

Commit a054a09

Browse files
committed
🥅 Return UnparsedData for unhandled response-data
Previously, unhandled `response-data` would attempt to parse to the end of the string using `text`. This would fail if the string contained either literals or binary data. The new `unparsed_response` approach simply grabs all remaining bytes (except for the final CRLF). Rather than simply return string data, the response's data uses a new `UnparsedData` struct, to unambiguously signal that the result may change if a future release starts parsing the string. The same approach has been copied over to `numeric_response`, allowing unhandled numeric responses to be handled more robustly as well. `IgnoredResponse` was converted to a subclass of `UntaggedResponse`, and assigns any remaining unparsed text to an `UnparsedData` struct. Other than the subclass, it is now treated like any other unknown/unparsed response type. +NOOP+ still returns IgnoredResponse.
1 parent 724fe71 commit a054a09

File tree

3 files changed

+60
-25
lines changed

3 files changed

+60
-25
lines changed

lib/net/imap/response_data.rb

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,32 @@ class UntaggedResponse < Struct.new(:name, :data, :raw_data)
5555

5656
# Net::IMAP::IgnoredResponse represents intentionally ignored responses.
5757
#
58-
# This includes untagged response "NOOP" sent by eg. Zimbra to avoid some
59-
# clients to close the connection.
58+
# This includes untagged response "NOOP" sent by eg. Zimbra to avoid
59+
# some clients to close the connection.
6060
#
6161
# It matches no IMAP standard.
62-
#
63-
class IgnoredResponse < Struct.new(:raw_data)
62+
class IgnoredResponse < UntaggedResponse
63+
end
64+
65+
# Net::IMAP::UnparsedData represents data for unknown or unhandled
66+
# response types. UnparsedData is an intentionally unstable API: where
67+
# it is returned, future releases may return a different (incompatible)
68+
# object without deprecation or warning.
69+
class UnparsedData < Struct.new(:number, :unparsed_data)
6470
##
65-
# method: raw_data
66-
# :call-seq: raw_data -> string
71+
# method: number
72+
# :call-seq: number -> integer
6773
#
68-
# The raw response data.
74+
# Returns a numeric response data prefix, when available.
75+
#
76+
# Many response types are prefixed with seqno or uid (for message
77+
# data) or a message count (for mailbox data).
78+
79+
##
80+
# method: unparsed_data
81+
# :call-seq: string -> string
82+
#
83+
# The unparsed data, not including #number or UntaggedResponse#name.
6984
end
7085

7186
# Net::IMAP::TaggedResponse represents tagged responses.

lib/net/imap/response_parser.rb

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -476,13 +476,29 @@ def response_data
476476
when /\A(?:ENABLED)\z/ni
477477
return enable_data
478478
else
479-
return text_response
479+
return unparsed_response
480480
end
481481
else
482482
parse_error("unexpected token %s", token.symbol)
483483
end
484484
end
485485

486+
def unparsed_response(klass = UntaggedResponse)
487+
num = number?; SP?
488+
type = tagged_ext_label; SP?
489+
text = remaining_unparsed
490+
data = UnparsedData.new(num, text) if num || text
491+
klass.new(type, data, @str)
492+
end
493+
494+
# reads all the way up until CRLF
495+
def remaining_unparsed
496+
str = @str[@pos...-2] and @pos += str.bytesize
497+
str&.empty? ? nil : str
498+
end
499+
500+
def ignored_response; unparsed_response(IgnoredResponse) end
501+
486502
# RFC3501 & RFC9051:
487503
# response-tagged = tag SP resp-cond-state CRLF
488504
#
@@ -517,6 +533,10 @@ def numeric_response
517533
match(T_SPACE)
518534
data = FetchData.new(n, msg_att(n))
519535
return UntaggedResponse.new(name, data, @str)
536+
else
537+
klass = name == "NOOP" ? IgnoredResponse : UntaggedResponse
538+
SP?; txt = remaining_unparsed
539+
klass.new(name, UnparsedData.new(n, txt), @str)
520540
end
521541
end
522542

@@ -976,20 +996,6 @@ def modseq_data
976996
return name, modseq
977997
end
978998

979-
def ignored_response
980-
while lookahead.symbol != T_CRLF
981-
shift_token
982-
end
983-
return IgnoredResponse.new(@str)
984-
end
985-
986-
def text_response
987-
token = match(T_ATOM)
988-
name = token.value.upcase
989-
match(T_SPACE)
990-
return UntaggedResponse.new(name, text)
991-
end
992-
993999
def flags_response
9941000
token = match(T_ATOM)
9951001
name = token.value.upcase
Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
---
22
:tests:
33
test_invalid_noop_response_is_ignored:
4-
:comment: |
5-
This should probably use UntaggedResponse, perhaps with an
6-
UnparsedResponseData object for its #data.
74
:response: "* NOOP\r\n"
85
:expected: !ruby/struct:Net::IMAP::IgnoredResponse
6+
name: "NOOP"
97
raw_data: "* NOOP\r\n"
8+
9+
test_invalid_noop_response_with_unparseable_data:
10+
:response: "* NOOP froopy snood\r\n"
11+
:expected: !ruby/struct:Net::IMAP::IgnoredResponse
12+
name: "NOOP"
13+
data: !ruby/struct:Net::IMAP::UnparsedData
14+
unparsed_data: "froopy snood"
15+
raw_data: "* NOOP froopy snood\r\n"
16+
17+
test_invalid_noop_response_with_numeric_prefix:
18+
:response: "* 99 NOOP\r\n"
19+
:expected: !ruby/struct:Net::IMAP::IgnoredResponse
20+
name: "NOOP"
21+
data: !ruby/struct:Net::IMAP::UnparsedData
22+
number: 99
23+
raw_data: "* 99 NOOP\r\n"

0 commit comments

Comments
 (0)