net-imap 0.5.9 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.document +3 -0
- data/.rdoc_options +7 -0
- data/Gemfile +7 -5
- data/README.md +1 -1
- data/lib/net/imap/command_data.rb +170 -80
- data/lib/net/imap/config/attr_accessors.rb +8 -9
- data/lib/net/imap/config/attr_inheritance.rb +64 -1
- data/lib/net/imap/config/attr_type_coercion.rb +18 -6
- data/lib/net/imap/config/attr_version_defaults.rb +90 -0
- data/lib/net/imap/config.rb +244 -122
- data/lib/net/imap/connection_state.rb +1 -1
- data/lib/net/imap/data_encoding.rb +126 -27
- data/lib/net/imap/errors.rb +189 -0
- data/lib/net/imap/esearch_result.rb +48 -3
- data/lib/net/imap/flags.rb +1 -1
- data/lib/net/imap/response_data.rb +110 -14
- data/lib/net/imap/response_parser/parser_utils.rb +14 -23
- data/lib/net/imap/response_parser.rb +40 -17
- data/lib/net/imap/response_reader.rb +25 -16
- data/lib/net/imap/sasl/scram_authenticator.rb +74 -0
- data/lib/net/imap/search_result.rb +13 -4
- data/lib/net/imap/sequence_set.rb +715 -326
- data/lib/net/imap/uidplus_data.rb +2 -63
- data/lib/net/imap/vanished_data.rb +10 -1
- data/lib/net/imap.rb +201 -86
- data/net-imap.gemspec +1 -1
- data/rakelib/rdoc.rake +1 -18
- metadata +6 -4
- data/lib/net/imap/data_lite.rb +0 -226
data/lib/net/imap/errors.rb
CHANGED
|
@@ -51,7 +51,196 @@ module Net
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
# Error raised when a response from the server is non-parsable.
|
|
54
|
+
#
|
|
55
|
+
# NOTE: Parser attributes are provided for debugging and inspection only.
|
|
56
|
+
# Their names and semantics may change incompatibly in any release.
|
|
54
57
|
class ResponseParseError < Error
|
|
58
|
+
# returns "" for all highlights
|
|
59
|
+
ESC_NO_HL = Hash.new("").freeze
|
|
60
|
+
private_constant :ESC_NO_HL
|
|
61
|
+
|
|
62
|
+
# Translates hash[:"/foo"] to hash[:reset] when hash.key?(:foo), else ""
|
|
63
|
+
#
|
|
64
|
+
# TODO: DRY this up with Config::AttrTypeCoercion.safe
|
|
65
|
+
if defined?(::Ractor.shareable_proc)
|
|
66
|
+
default_highlight = Ractor.shareable_proc {|hash, key|
|
|
67
|
+
%r{\A/(.+)} =~ key && hash.key?($1.to_sym) ? hash[:reset] : ""
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
default_highlight = nil.instance_eval { Proc.new {|hash, key|
|
|
71
|
+
%r{\A/(.+)} =~ key && hash.key?($1.to_sym) ? hash[:reset] : ""
|
|
72
|
+
} }
|
|
73
|
+
::Ractor.make_shareable(default_highlight) if defined?(::Ractor)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# ANSI highlights, but no colors
|
|
77
|
+
ESC_NO_COLOR = Hash.new(&default_highlight).update(
|
|
78
|
+
reset: "\e[m",
|
|
79
|
+
val: "\e[1m", # bold
|
|
80
|
+
alt: "\e[1;4m", # bold and underlined
|
|
81
|
+
sym: "\e[1m", # bold
|
|
82
|
+
label: "\e[1m", # bold
|
|
83
|
+
).freeze
|
|
84
|
+
private_constant :ESC_NO_COLOR
|
|
85
|
+
|
|
86
|
+
# ANSI highlights, with color
|
|
87
|
+
ESC_COLORS = Hash.new(&default_highlight).update(
|
|
88
|
+
reset: "\e[m",
|
|
89
|
+
key: "\e[95m", # bright magenta
|
|
90
|
+
idx: "\e[34m", # blue
|
|
91
|
+
val: "\e[36;40m", # cyan on black (to ensure contrast)
|
|
92
|
+
alt: "\e[1;33;40m", # bold; yellow on black
|
|
93
|
+
sym: "\e[33;40m", # yellow on black
|
|
94
|
+
label: "\e[1m", # bold
|
|
95
|
+
nil: "\e[35m", # magenta
|
|
96
|
+
).freeze
|
|
97
|
+
private_constant :ESC_COLORS
|
|
98
|
+
|
|
99
|
+
# Net::IMAP::ResponseParser, unless a custom parser produced the error.
|
|
100
|
+
attr_reader :parser_class
|
|
101
|
+
|
|
102
|
+
# The full raw response string which was being parsed.
|
|
103
|
+
attr_reader :string
|
|
104
|
+
|
|
105
|
+
# The parser's byte position in #string when the error was raised.
|
|
106
|
+
#
|
|
107
|
+
# _NOTE:_ This attribute is provided for debugging and inspection only.
|
|
108
|
+
# Its name and semantics may change incompatibly in any release.
|
|
109
|
+
attr_reader :pos
|
|
110
|
+
|
|
111
|
+
# The parser's lex state
|
|
112
|
+
#
|
|
113
|
+
# _NOTE:_ This attribute is provided for debugging and inspection only.
|
|
114
|
+
# Its name and semantics may change incompatibly in any release.
|
|
115
|
+
attr_reader :lex_state
|
|
116
|
+
|
|
117
|
+
# The last lexed token
|
|
118
|
+
#
|
|
119
|
+
# May be +nil+ when the parser has accepted the last token and peeked at
|
|
120
|
+
# the next byte without generating a token.
|
|
121
|
+
#
|
|
122
|
+
# _NOTE:_ This attribute is provided for debugging and inspection only.
|
|
123
|
+
# Its name and semantics may change incompatibly in any release.
|
|
124
|
+
attr_reader :token
|
|
125
|
+
|
|
126
|
+
def initialize(message = "unspecified parse error",
|
|
127
|
+
parser_class: Net::IMAP::ResponseParser,
|
|
128
|
+
parser_state: nil,
|
|
129
|
+
string: parser_state&.at(0), # see ParserUtils#parser_state
|
|
130
|
+
lex_state: parser_state&.at(1), # see ParserUtils#parser_state
|
|
131
|
+
pos: parser_state&.at(2), # see ParserUtils#parser_state
|
|
132
|
+
token: parser_state&.at(3)) # see ParserUtils#parser_state
|
|
133
|
+
@parser_class = parser_class
|
|
134
|
+
@string = string
|
|
135
|
+
@pos = pos
|
|
136
|
+
@lex_state = lex_state
|
|
137
|
+
@token = token
|
|
138
|
+
super(message)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# When +parser_state+ is true, debug info about the parser state is
|
|
142
|
+
# included. Defaults to the value of Net::IMAP.debug.
|
|
143
|
+
#
|
|
144
|
+
# When +parser_backtrace+ is true, a simplified backtrace is included,
|
|
145
|
+
# containing only frames for methods in parser_class (since ruby 3.4) or
|
|
146
|
+
# which have "net/imap/response_parser" in the path (before ruby 3.4).
|
|
147
|
+
# Most parser method names are based on rules in the IMAP grammar.
|
|
148
|
+
#
|
|
149
|
+
# When +highlight+ is not explicitly set, highlights may be enabled
|
|
150
|
+
# automatically, based on +TERM+ and +FORCE_COLOR+ environment variables.
|
|
151
|
+
#
|
|
152
|
+
# By default, +highlight+ uses colors from the basic ANSI palette. When
|
|
153
|
+
# +highlight_no_color+ is true or the +NO_COLOR+ environment variable is
|
|
154
|
+
# not empty, only monochromatic highlights are used: bold, underline, etc.
|
|
155
|
+
def detailed_message(parser_state: Net::IMAP.debug,
|
|
156
|
+
parser_backtrace: false,
|
|
157
|
+
highlight: default_highlight_from_env,
|
|
158
|
+
highlight_no_color: (ENV["NO_COLOR"] || "") != "",
|
|
159
|
+
**)
|
|
160
|
+
return super unless parser_state || parser_backtrace
|
|
161
|
+
msg = super.dup
|
|
162
|
+
esc = !highlight ? ESC_NO_HL : highlight_no_color ? ESC_NO_COLOR : ESC_COLORS
|
|
163
|
+
hl = ->str { str % esc }
|
|
164
|
+
val = ->str, val { hl[val.nil? ? "%{nil}%%p%{/nil}" : str] % val }
|
|
165
|
+
if parser_state && (string || pos || lex_state || token)
|
|
166
|
+
msg << hl["\n %{key}processed %{/key}: "] << val["%{val}%%p%{/val}", processed_string]
|
|
167
|
+
msg << hl["\n %{key}remaining %{/key}: "] << val["%{alt}%%p%{/alt}", remaining_string]
|
|
168
|
+
msg << hl["\n %{key}pos %{/key}: "] << val["%{val}%%p%{/val}", pos]
|
|
169
|
+
msg << hl["\n %{key}lex_state %{/key}: "] << val["%{sym}%%p%{/sym}", lex_state]
|
|
170
|
+
msg << hl["\n %{key}token %{/key}: "] << val[
|
|
171
|
+
"%{sym}%%<symbol>p%{/sym} => %{val}%%<value>p%{/val}", token&.to_h
|
|
172
|
+
]
|
|
173
|
+
end
|
|
174
|
+
if parser_backtrace
|
|
175
|
+
normalized_parser_backtrace.each do |idx, path, lineno, label, base_label|
|
|
176
|
+
msg << "\n %s: %s (%s:%d)" % [
|
|
177
|
+
hl["%{key}caller[%{/key}%{idx}%%2d%{/idx}%{key}]%{/key}"] % idx,
|
|
178
|
+
hl["%{label}%%-30s%{/label}"] % base_label,
|
|
179
|
+
File.basename(path, ".rb"), lineno
|
|
180
|
+
]
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
msg
|
|
184
|
+
rescue => error
|
|
185
|
+
msg ||= super.dup
|
|
186
|
+
msg << "\n BUG in %s#%s: %s" % [self.class, __method__,
|
|
187
|
+
error.detailed_message]
|
|
188
|
+
msg
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def processed_string = string && pos && string[...pos]
|
|
192
|
+
def remaining_string = string && pos && string[pos..]
|
|
193
|
+
|
|
194
|
+
# Returns true when all attributes are equal, except for #backtrace and
|
|
195
|
+
# #backtrace_locations which are replaced with #parser_methods. This
|
|
196
|
+
# allows deserialized errors to be compared.
|
|
197
|
+
def ==(other)
|
|
198
|
+
return false if self.class != other.class
|
|
199
|
+
methods = parser_methods
|
|
200
|
+
other_methods = other.parser_methods
|
|
201
|
+
message == other.message &&
|
|
202
|
+
methods == other_methods &&
|
|
203
|
+
string == other.string &&
|
|
204
|
+
pos == other.pos &&
|
|
205
|
+
lex_state == other.lex_state &&
|
|
206
|
+
token == other.token
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Lists the methods (from #backtrace_locations or #backtrace) called on
|
|
210
|
+
# parser_class (since ruby 3.4) or which have "net/imap/response_parser"
|
|
211
|
+
# in the path (before ruby 3.4). Most parser method names are based on
|
|
212
|
+
# rules in the IMAP grammar.
|
|
213
|
+
def parser_methods = normalized_parser_backtrace.map(&:last)
|
|
214
|
+
|
|
215
|
+
private
|
|
216
|
+
|
|
217
|
+
def normalized_parser_backtrace
|
|
218
|
+
normalize_backtrace
|
|
219
|
+
.take_while {|_, _, _, _, base_label| base_label != "parse" }
|
|
220
|
+
.reject {|_, _, _, _, base_label| base_label.nil? }
|
|
221
|
+
.reject {|_, _, _, _, base_label| base_label.include? "parse_error" }
|
|
222
|
+
.select {|_, path, _, label, _|
|
|
223
|
+
if label.include?("#") # => Class#method, since ruby 3.4
|
|
224
|
+
label.include?(parser_class.name)
|
|
225
|
+
else
|
|
226
|
+
path.include?("net/imap/response_parser")
|
|
227
|
+
end
|
|
228
|
+
}
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def normalize_backtrace
|
|
232
|
+
(backtrace_locations&.each_with_index&.map {|loc, idx|
|
|
233
|
+
[idx, loc.path, loc.lineno, loc.label, loc.base_label]
|
|
234
|
+
} || backtrace&.each_with_index&.map {|bt, idx|
|
|
235
|
+
[idx, *bt.match(/\A(\S+):(\d+):in [`'](.*?([\w]+[?!]?))'\z/)&.captures]
|
|
236
|
+
} || [])
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def default_highlight_from_env
|
|
240
|
+
(ENV["FORCE_COLOR"] || "") !~ /\A(?:0|)\z/ ||
|
|
241
|
+
(ENV["TERM"] || "") !~ /\A(?:dumb|unknown|)\z/i
|
|
242
|
+
end
|
|
243
|
+
|
|
55
244
|
end
|
|
56
245
|
|
|
57
246
|
# Superclass of all errors used to encapsulate "fail" responses
|
|
@@ -25,6 +25,12 @@ module Net
|
|
|
25
25
|
# Some search extensions may result in the server sending ESearchResult
|
|
26
26
|
# responses after the initiating command has completed. Use
|
|
27
27
|
# IMAP#add_response_handler to handle these responses.
|
|
28
|
+
#
|
|
29
|
+
# ==== Compatibility with SearchResult
|
|
30
|
+
#
|
|
31
|
+
# Note that both SearchResult and ESearchResult implement +each+, +to_a+,
|
|
32
|
+
# and +to_sequence_set+. These methods can be used regardless of whether
|
|
33
|
+
# the server returns +SEARCH+ or +ESEARCH+ data (or no data).
|
|
28
34
|
class ESearchResult < Data.define(:tag, :uid, :data)
|
|
29
35
|
def initialize(tag: nil, uid: nil, data: nil)
|
|
30
36
|
tag => String | nil; tag = -tag if tag
|
|
@@ -39,12 +45,49 @@ module Net
|
|
|
39
45
|
# numbers or UIDs, +to_a+ returns that set as an array of integers.
|
|
40
46
|
#
|
|
41
47
|
# When both #all and #partial are +nil+, either because the server
|
|
42
|
-
# returned no results or because +ALL+
|
|
43
|
-
# the IMAP#search +RETURN+ options, #to_a returns an empty array.
|
|
48
|
+
# returned no results or because neither +ALL+ or +PARTIAL+ were included
|
|
49
|
+
# in the IMAP#search +RETURN+ options, #to_a returns an empty array.
|
|
44
50
|
#
|
|
45
51
|
# Note that SearchResult also implements +to_a+, so it can be used without
|
|
46
52
|
# checking if the server returned +SEARCH+ or +ESEARCH+ data.
|
|
47
|
-
|
|
53
|
+
#
|
|
54
|
+
# Related: #each, #to_sequence_set, #all, #partial
|
|
55
|
+
def to_a; to_sequence_set.numbers end
|
|
56
|
+
|
|
57
|
+
# :call-seq: to_sequence_set -> SequenceSet or nil
|
|
58
|
+
#
|
|
59
|
+
# When either #all or #partial contains a SequenceSet of message sequence
|
|
60
|
+
# numbers or UIDs, +to_sequence_set+ returns that sequence set.
|
|
61
|
+
#
|
|
62
|
+
# When both #all and #partial are +nil+, either because the server
|
|
63
|
+
# returned no results or because neither +ALL+ or +PARTIAL+ were included
|
|
64
|
+
# in the IMAP#search +RETURN+ options, #to_sequence_set returns
|
|
65
|
+
# SequenceSet.empty.
|
|
66
|
+
#
|
|
67
|
+
# Note that SearchResult also implements +to_sequence_set+, so it can be
|
|
68
|
+
# used without checking if the server returned +SEARCH+ or +ESEARCH+ data.
|
|
69
|
+
#
|
|
70
|
+
# Related: #each, #to_a, #all, #partial
|
|
71
|
+
def to_sequence_set
|
|
72
|
+
all || partial&.to_sequence_set || SequenceSet.empty
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# When either #all or #partial contains a SequenceSet of message sequence
|
|
76
|
+
# numbers or UIDs, +each+ yields each integer in the set.
|
|
77
|
+
#
|
|
78
|
+
# When both #all and #partial are +nil+, either because the server
|
|
79
|
+
# returned no results or because +ALL+ and +PARTIAL+ were not included in
|
|
80
|
+
# the IMAP#search +RETURN+ options, #each does not yield.
|
|
81
|
+
#
|
|
82
|
+
# Note that SearchResult also implements +#each+, so it can be used
|
|
83
|
+
# without checking if the server returned +SEARCH+ or +ESEARCH+ data.
|
|
84
|
+
#
|
|
85
|
+
# Related: #to_sequence_set, #to_a, #all, #partial
|
|
86
|
+
def each(&)
|
|
87
|
+
return to_enum(__callee__) unless block_given?
|
|
88
|
+
to_sequence_set.each_number(&)
|
|
89
|
+
self
|
|
90
|
+
end
|
|
48
91
|
|
|
49
92
|
##
|
|
50
93
|
# attr_reader: tag
|
|
@@ -161,6 +204,8 @@ module Net
|
|
|
161
204
|
#
|
|
162
205
|
# See also: ESearchResult#to_a.
|
|
163
206
|
def to_a; results&.numbers || [] end
|
|
207
|
+
|
|
208
|
+
alias to_sequence_set results
|
|
164
209
|
end
|
|
165
210
|
|
|
166
211
|
# :call-seq: partial -> PartialResult or nil
|
data/lib/net/imap/flags.rb
CHANGED
|
@@ -6,7 +6,6 @@ module Net
|
|
|
6
6
|
autoload :FetchData, "#{__dir__}/fetch_data"
|
|
7
7
|
autoload :UIDFetchData, "#{__dir__}/fetch_data"
|
|
8
8
|
autoload :SearchResult, "#{__dir__}/search_result"
|
|
9
|
-
autoload :UIDPlusData, "#{__dir__}/uidplus_data"
|
|
10
9
|
autoload :AppendUIDData, "#{__dir__}/uidplus_data"
|
|
11
10
|
autoload :CopyUIDData, "#{__dir__}/uidplus_data"
|
|
12
11
|
autoload :VanishedData, "#{__dir__}/vanished_data"
|
|
@@ -76,9 +75,18 @@ module Net
|
|
|
76
75
|
#
|
|
77
76
|
# Net::IMAP::UnparsedData represents data for unknown response types or
|
|
78
77
|
# unknown extensions to response types without a well-defined extension
|
|
79
|
-
# grammar.
|
|
78
|
+
# grammar. UnparsedData represents the portion of the response which the
|
|
79
|
+
# parser has skipped over, without attempting to parse it.
|
|
80
80
|
#
|
|
81
|
-
#
|
|
81
|
+
# parser = Net::IMAP::ResponseParser.new
|
|
82
|
+
# response = parser.parse "* X-UNKNOWN-TYPE can't parse this\r\n"
|
|
83
|
+
# response => Net::IMAP::UntaggedResponse(
|
|
84
|
+
# name: "X-UNKNOWN-TYPE",
|
|
85
|
+
# data: Net::IMAP::UnparsedData(unparsed_data: "can't parse this"),
|
|
86
|
+
# )
|
|
87
|
+
#
|
|
88
|
+
# See also: UnparsedNumericResponseData, ExtensionData, IgnoredResponse,
|
|
89
|
+
# InvalidParseData.
|
|
82
90
|
class UnparsedData < Struct.new(:unparsed_data)
|
|
83
91
|
##
|
|
84
92
|
# method: unparsed_data
|
|
@@ -87,6 +95,61 @@ module Net
|
|
|
87
95
|
# The unparsed data
|
|
88
96
|
end
|
|
89
97
|
|
|
98
|
+
# **Note:** This represents an intentionally _unstable_ API. Where
|
|
99
|
+
# instances of this class are returned, future releases may return a
|
|
100
|
+
# different (incompatible) object <em>without deprecation or warning</em>.
|
|
101
|
+
#
|
|
102
|
+
# When the response parser encounters a recoverable error,
|
|
103
|
+
# Net::IMAP::InvalidParseData represents that portion of the response which
|
|
104
|
+
# could not be parsed, allowing the parser to parse the remainder of the
|
|
105
|
+
# response. InvalidParseData is always associated with a ResponseParseError
|
|
106
|
+
# which has been rescued.
|
|
107
|
+
#
|
|
108
|
+
# This could be caused by a malformed server response, by a bug in
|
|
109
|
+
# Net::IMAP::ResponseParser, or by an unsupported extension to the response
|
|
110
|
+
# syntax. For example, if a server supports +UIDPLUS+, but sends an invalid
|
|
111
|
+
# +COPYUID+ response code:
|
|
112
|
+
#
|
|
113
|
+
# parser = Net::IMAP::ResponseParser.new
|
|
114
|
+
# parsed = parser.parse "* OK [COPYUID 701 ] copied one message\r\n"
|
|
115
|
+
# parsed => {
|
|
116
|
+
# data: Net::IMAP::ResponseText(
|
|
117
|
+
# code: Net::IMAP::ResponseCode(
|
|
118
|
+
# name: "COPYUID",
|
|
119
|
+
# data: Net::IMAP::InvalidParseData(
|
|
120
|
+
# parse_error: Net::IMAP::ResponseParseError,
|
|
121
|
+
# unparsed_data: "701 ",
|
|
122
|
+
# parsed_data: nil,
|
|
123
|
+
# )
|
|
124
|
+
# )
|
|
125
|
+
# )
|
|
126
|
+
# }
|
|
127
|
+
#
|
|
128
|
+
# In this example, although <tt>[COPYUID 701 ]</tt> uses valid syntax for a
|
|
129
|
+
# _generic_ ResponseCode, it is _invalid_ syntax for a +COPYUID+ response
|
|
130
|
+
# code.
|
|
131
|
+
#
|
|
132
|
+
# See also: UnparsedData, ExtensionData
|
|
133
|
+
class InvalidParseData < Data.define(:parse_error, :unparsed_data, :parsed_data)
|
|
134
|
+
##
|
|
135
|
+
# method: parse_error
|
|
136
|
+
# :call-seq: parse_error -> ResponseParseError
|
|
137
|
+
#
|
|
138
|
+
# Returns the rescued ResponseParseError.
|
|
139
|
+
|
|
140
|
+
##
|
|
141
|
+
# method: unparsed_data
|
|
142
|
+
# :call-seq: unparsed_data -> string
|
|
143
|
+
#
|
|
144
|
+
# Returns the raw string which was skipped over by the parser.
|
|
145
|
+
|
|
146
|
+
##
|
|
147
|
+
# method: parsed_data
|
|
148
|
+
#
|
|
149
|
+
# May return a partial parse result for unparsed_data, which had already
|
|
150
|
+
# been parsed before the parse_error.
|
|
151
|
+
end
|
|
152
|
+
|
|
90
153
|
# **Note:** This represents an intentionally _unstable_ API. Where
|
|
91
154
|
# instances of this class are returned, future releases may return a
|
|
92
155
|
# different (incompatible) object <em>without deprecation or warning</em>.
|
|
@@ -94,7 +157,17 @@ module Net
|
|
|
94
157
|
# Net::IMAP::UnparsedNumericResponseData represents data for unhandled
|
|
95
158
|
# response types with a numeric prefix. See the documentation for #number.
|
|
96
159
|
#
|
|
97
|
-
#
|
|
160
|
+
# parser = Net::IMAP::ResponseParser.new
|
|
161
|
+
# response = parser.parse "* 123 X-UNKNOWN-TYPE can't parse this\r\n"
|
|
162
|
+
# response => Net::IMAP::UntaggedResponse(
|
|
163
|
+
# name: "X-UNKNOWN-TYPE",
|
|
164
|
+
# data: Net::IMAP::UnparsedNumericData(
|
|
165
|
+
# number: 123,
|
|
166
|
+
# unparsed_data: "can't parse this"
|
|
167
|
+
# ),
|
|
168
|
+
# )
|
|
169
|
+
#
|
|
170
|
+
# See also: UnparsedData, ExtensionData, IgnoredResponse, InvalidParseData
|
|
98
171
|
class UnparsedNumericResponseData < Struct.new(:number, :unparsed_data)
|
|
99
172
|
##
|
|
100
173
|
# method: number
|
|
@@ -260,8 +333,8 @@ module Net
|
|
|
260
333
|
#
|
|
261
334
|
# === +UIDPLUS+ extension
|
|
262
335
|
# See {[RFC4315 §3]}[https://www.rfc-editor.org/rfc/rfc4315#section-3].
|
|
263
|
-
# * +APPENDUID+, #data is
|
|
264
|
-
# * +COPYUID+, #data is
|
|
336
|
+
# * +APPENDUID+, #data is AppendUIDData. See IMAP#append.
|
|
337
|
+
# * +COPYUID+, #data is CopyUIDData. See IMAP#copy.
|
|
265
338
|
# * +UIDNOTSTICKY+, #data is +nil+. See IMAP#select.
|
|
266
339
|
#
|
|
267
340
|
# === +SEARCHRES+ extension
|
|
@@ -307,6 +380,14 @@ module Net
|
|
|
307
380
|
# because the server doesn't allow deletion of mailboxes with children.
|
|
308
381
|
# #data is +nil+.
|
|
309
382
|
#
|
|
383
|
+
# === <tt>QUOTA=RES-*</tt> response codes
|
|
384
|
+
# See {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html#section-4.3].
|
|
385
|
+
# * +OVERQUOTA+ (also in RFC5530[https://www.rfc-editor.org/rfc/rfc5530]),
|
|
386
|
+
# with a tagged +NO+ response to an +APPEND+/+COPY+/+MOVE+ command when
|
|
387
|
+
# the command would put the target mailbox over any quota, and with an
|
|
388
|
+
# untagged +NO+ when a mailbox exceeds a soft quota (which may be caused
|
|
389
|
+
# be external events). #data is +nil+.
|
|
390
|
+
#
|
|
310
391
|
# === +CONDSTORE+ extension
|
|
311
392
|
# See {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
|
|
312
393
|
# * +NOMODSEQ+, when selecting a mailbox that does not support
|
|
@@ -325,9 +406,10 @@ module Net
|
|
|
325
406
|
#
|
|
326
407
|
# Response codes are backwards compatible: Servers are allowed to send new
|
|
327
408
|
# response codes even if the client has not enabled the extension that
|
|
328
|
-
# defines them. When
|
|
329
|
-
# code
|
|
330
|
-
#
|
|
409
|
+
# defines them. When ResponseParser does not know how to parse the response
|
|
410
|
+
# code data, #data may return the unparsed string, ExtensionData, or
|
|
411
|
+
# UnparsedData. When ResponseParser attempts but fails to parse the
|
|
412
|
+
# response code data, #data returns InvalidParseData.
|
|
331
413
|
class ResponseCode < Struct.new(:name, :data)
|
|
332
414
|
##
|
|
333
415
|
# method: name
|
|
@@ -342,8 +424,13 @@ module Net
|
|
|
342
424
|
#
|
|
343
425
|
# Returns the parsed response code data, e.g: an array of capabilities
|
|
344
426
|
# strings, an array of character set strings, a list of permanent flags,
|
|
345
|
-
# an Integer, etc. The response #
|
|
346
|
-
# code data can take.
|
|
427
|
+
# an Integer, etc. The response #name determines what form the response
|
|
428
|
+
# code #data can take.
|
|
429
|
+
#
|
|
430
|
+
# When ResponseParser does not know how to parse the response code data,
|
|
431
|
+
# #data may return the unparsed string, ExtensionData, or UnparsedData.
|
|
432
|
+
# When ResponseParser attempts but fails to parse the response code data,
|
|
433
|
+
# #data returns InvalidParseData.
|
|
347
434
|
end
|
|
348
435
|
|
|
349
436
|
# MailboxList represents the data of an untagged +LIST+ response, for a
|
|
@@ -384,14 +471,23 @@ module Net
|
|
|
384
471
|
# and MailboxQuota objects.
|
|
385
472
|
#
|
|
386
473
|
# == Required capability
|
|
474
|
+
#
|
|
387
475
|
# Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
|
|
388
|
-
#
|
|
476
|
+
# or <tt>QUOTA=RES-STORAGE</tt>
|
|
477
|
+
# [RFC9208[https://www.rfc-editor.org/rfc/rfc9208]] capability.
|
|
389
478
|
class MailboxQuota < Struct.new(:mailbox, :usage, :quota)
|
|
390
479
|
##
|
|
391
480
|
# method: mailbox
|
|
392
481
|
# :call-seq: mailbox -> string
|
|
393
482
|
#
|
|
394
|
-
# The
|
|
483
|
+
# The quota root with the associated quota.
|
|
484
|
+
#
|
|
485
|
+
# NOTE: this was mistakenly named "mailbox". But the quota root's name may
|
|
486
|
+
# differ from the mailbox. A single quota root may cover multiple
|
|
487
|
+
# mailboxes, and a single mailbox may be governed by multiple quota roots.
|
|
488
|
+
|
|
489
|
+
# The quota root with the associated quota.
|
|
490
|
+
alias quota_root mailbox
|
|
395
491
|
|
|
396
492
|
##
|
|
397
493
|
# method: usage
|
|
@@ -403,7 +499,7 @@ module Net
|
|
|
403
499
|
# method: quota
|
|
404
500
|
# :call-seq: quota -> Integer
|
|
405
501
|
#
|
|
406
|
-
#
|
|
502
|
+
# Storage limit imposed on the mailbox.
|
|
407
503
|
#
|
|
408
504
|
end
|
|
409
505
|
|
|
@@ -215,29 +215,20 @@ module Net
|
|
|
215
215
|
@token = nil
|
|
216
216
|
end
|
|
217
217
|
|
|
218
|
-
def parse_error(fmt, *args)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
idx,
|
|
233
|
-
cloc.base_label,
|
|
234
|
-
File.basename(cloc.path, ".rb"),
|
|
235
|
-
cloc.lineno
|
|
236
|
-
]
|
|
237
|
-
end
|
|
238
|
-
end
|
|
239
|
-
raise ResponseParseError, msg
|
|
240
|
-
end
|
|
218
|
+
def parse_error(fmt, *args) = raise exception format(fmt, *args)
|
|
219
|
+
|
|
220
|
+
def exception(message) = ResponseParseError.new(
|
|
221
|
+
message, parser_state:, parser_class: self.class
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# This can be used to backtrack after a parse error, and re-attempt to
|
|
225
|
+
# parse using a fallback.
|
|
226
|
+
#
|
|
227
|
+
# NOTE: Reckless backtracking could lead to O(n²) situations, so this
|
|
228
|
+
# should very rarely be used. Ideally, fallbacks should not backtrack.
|
|
229
|
+
def restore_state(state) = (@lex_state, @pos, @token = state)
|
|
230
|
+
def current_state = [@lex_state, @pos, @token]
|
|
231
|
+
def parser_state = [@str, *current_state]
|
|
241
232
|
|
|
242
233
|
end
|
|
243
234
|
end
|
|
@@ -38,6 +38,11 @@ module Net
|
|
|
38
38
|
@lex_state = EXPR_BEG
|
|
39
39
|
@token = nil
|
|
40
40
|
return response
|
|
41
|
+
rescue ResponseParseError => error
|
|
42
|
+
if config.debug?
|
|
43
|
+
warn error.detailed_message(parser_state: true, parser_backtrace: true)
|
|
44
|
+
end
|
|
45
|
+
raise
|
|
41
46
|
end
|
|
42
47
|
|
|
43
48
|
private
|
|
@@ -686,6 +691,8 @@ module Net
|
|
|
686
691
|
CRLF!
|
|
687
692
|
EOF!
|
|
688
693
|
resp
|
|
694
|
+
rescue SystemStackError
|
|
695
|
+
parse_error("response recursion too deep")
|
|
689
696
|
end
|
|
690
697
|
|
|
691
698
|
# RFC3501 & RFC9051:
|
|
@@ -1883,12 +1890,17 @@ module Net
|
|
|
1883
1890
|
# We leniently re-interpret this as
|
|
1884
1891
|
# resp-text = ["[" resp-text-code "]" [SP [text]] / [text]
|
|
1885
1892
|
def resp_text
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1893
|
+
begin
|
|
1894
|
+
state = current_state
|
|
1895
|
+
if lbra?
|
|
1896
|
+
code = resp_text_code; rbra
|
|
1897
|
+
return ResponseText.new(code, SP? && text? || "")
|
|
1898
|
+
end
|
|
1899
|
+
rescue ResponseParseError => error
|
|
1900
|
+
raise if /\buid-set\b/i.match? error.message
|
|
1901
|
+
restore_state state
|
|
1891
1902
|
end
|
|
1903
|
+
ResponseText.new(nil, text? || "")
|
|
1892
1904
|
end
|
|
1893
1905
|
|
|
1894
1906
|
# RFC3501 (See https://www.rfc-editor.org/errata/rfc3501):
|
|
@@ -1951,6 +1963,7 @@ module Net
|
|
|
1951
1963
|
# resp-text-code =/ "UIDREQUIRED"
|
|
1952
1964
|
def resp_text_code
|
|
1953
1965
|
name = resp_text_code__name
|
|
1966
|
+
state = current_state
|
|
1954
1967
|
data =
|
|
1955
1968
|
case name
|
|
1956
1969
|
when "CAPABILITY" then resp_code__capability
|
|
@@ -1973,8 +1986,18 @@ module Net
|
|
|
1973
1986
|
when "MAILBOXID" then SP!; parens__objectid # RFC8474: OBJECTID
|
|
1974
1987
|
when "UIDREQUIRED" then # RFC9586: UIDONLY
|
|
1975
1988
|
else
|
|
1989
|
+
state = nil # don't backtrack
|
|
1976
1990
|
SP? and text_chars_except_rbra
|
|
1977
1991
|
end
|
|
1992
|
+
peek_rbra? or
|
|
1993
|
+
parse_error("expected resp-text-code %p to be complete", name)
|
|
1994
|
+
ResponseCode.new(name, data)
|
|
1995
|
+
rescue Net::IMAP::ResponseParseError => parse_error
|
|
1996
|
+
raise unless state
|
|
1997
|
+
raise if parse_error.message.include?("uid-set")
|
|
1998
|
+
restore_state state
|
|
1999
|
+
unparsed_data = SP? && text_chars_except_rbra
|
|
2000
|
+
data = InvalidParseData[parse_error:, unparsed_data:, parsed_data: data]
|
|
1978
2001
|
ResponseCode.new(name, data)
|
|
1979
2002
|
end
|
|
1980
2003
|
|
|
@@ -2017,24 +2040,24 @@ module Net
|
|
|
2017
2040
|
CopyUID(validity, src_uids, dst_uids)
|
|
2018
2041
|
end
|
|
2019
2042
|
|
|
2020
|
-
|
|
2021
|
-
def CopyUID(...) DeprecatedUIDPlus(...) || CopyUIDData.new(...) end
|
|
2043
|
+
PARSER_PATH = File.expand_path(__FILE__).delete_suffix(".rb")
|
|
2022
2044
|
|
|
2023
2045
|
# TODO: remove this code in the v0.6.0 release
|
|
2024
2046
|
def DeprecatedUIDPlus(validity, src_uids = nil, dst_uids)
|
|
2025
2047
|
return unless config.parser_use_deprecated_uidplus_data
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
parse_error("uid-set is too large: %d > %d", count, max)
|
|
2035
|
-
end
|
|
2048
|
+
uplevel = caller_locations
|
|
2049
|
+
.find_index { !_1.path.start_with?(PARSER_PATH) }
|
|
2050
|
+
&.succ
|
|
2051
|
+
warn("#{Config}#parser_use_deprecated_uidplus_data is ignored " \
|
|
2052
|
+
"since v0.6.0. Disable this warning by setting " \
|
|
2053
|
+
"config.parser_use_deprecated_uidplus_data = false.",
|
|
2054
|
+
category: :deprecated, uplevel:)
|
|
2055
|
+
nil
|
|
2036
2056
|
end
|
|
2037
2057
|
|
|
2058
|
+
def AppendUID(...) DeprecatedUIDPlus(...) || AppendUIDData.new(...) end
|
|
2059
|
+
def CopyUID(...) DeprecatedUIDPlus(...) || CopyUIDData.new(...) end
|
|
2060
|
+
|
|
2038
2061
|
ADDRESS_REGEXP = /\G
|
|
2039
2062
|
\( (?: NIL | #{Patterns::QUOTED_rev2} ) # 1: NAME
|
|
2040
2063
|
\s (?: NIL | #{Patterns::QUOTED_rev2} ) # 2: ROUTE
|