net-imap 0.4.22 → 0.6.3

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +12 -2
  3. data/README.md +10 -4
  4. data/docs/styles.css +75 -14
  5. data/lib/net/imap/authenticators.rb +2 -2
  6. data/lib/net/imap/command_data.rb +40 -95
  7. data/lib/net/imap/config/attr_accessors.rb +8 -9
  8. data/lib/net/imap/config/attr_inheritance.rb +64 -1
  9. data/lib/net/imap/config/attr_type_coercion.rb +22 -10
  10. data/lib/net/imap/config/attr_version_defaults.rb +90 -0
  11. data/lib/net/imap/config.rb +241 -125
  12. data/lib/net/imap/connection_state.rb +48 -0
  13. data/lib/net/imap/data_encoding.rb +80 -31
  14. data/lib/net/imap/deprecated_client_options.rb +6 -3
  15. data/lib/net/imap/errors.rb +158 -0
  16. data/lib/net/imap/esearch_result.rb +225 -0
  17. data/lib/net/imap/fetch_data.rb +126 -47
  18. data/lib/net/imap/flags.rb +1 -1
  19. data/lib/net/imap/response_data.rb +123 -187
  20. data/lib/net/imap/response_parser/parser_utils.rb +19 -23
  21. data/lib/net/imap/response_parser.rb +182 -38
  22. data/lib/net/imap/response_reader.rb +10 -12
  23. data/lib/net/imap/sasl/anonymous_authenticator.rb +3 -3
  24. data/lib/net/imap/sasl/authentication_exchange.rb +52 -20
  25. data/lib/net/imap/sasl/authenticators.rb +8 -4
  26. data/lib/net/imap/sasl/client_adapter.rb +77 -26
  27. data/lib/net/imap/sasl/cram_md5_authenticator.rb +4 -4
  28. data/lib/net/imap/sasl/digest_md5_authenticator.rb +218 -56
  29. data/lib/net/imap/sasl/external_authenticator.rb +2 -2
  30. data/lib/net/imap/sasl/gs2_header.rb +7 -7
  31. data/lib/net/imap/sasl/login_authenticator.rb +4 -3
  32. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +6 -6
  33. data/lib/net/imap/sasl/plain_authenticator.rb +7 -7
  34. data/lib/net/imap/sasl/protocol_adapters.rb +60 -4
  35. data/lib/net/imap/sasl/scram_authenticator.rb +8 -8
  36. data/lib/net/imap/sasl.rb +7 -4
  37. data/lib/net/imap/sasl_adapter.rb +0 -1
  38. data/lib/net/imap/search_result.rb +10 -5
  39. data/lib/net/imap/sequence_set.rb +1104 -421
  40. data/lib/net/imap/stringprep/nameprep.rb +1 -1
  41. data/lib/net/imap/stringprep/trace.rb +4 -4
  42. data/lib/net/imap/uidplus_data.rb +4 -147
  43. data/lib/net/imap/vanished_data.rb +65 -0
  44. data/lib/net/imap.rb +1002 -313
  45. data/net-imap.gemspec +1 -1
  46. data/rakelib/rfcs.rake +2 -0
  47. data/rakelib/string_prep_tables_generator.rb +6 -2
  48. metadata +7 -3
@@ -155,57 +155,106 @@ module Net
155
155
 
156
156
  # Common validators of number and nz_number types
157
157
  module NumValidator # :nodoc
158
+ NUMBER_RE = /\A(?:0|[1-9]\d*)\z/
158
159
  module_function
159
160
 
160
- # Check is passed argument valid 'number' in RFC 3501 terminology
161
+ # Check if argument is a valid 'number' according to RFC 3501
162
+ # number = 1*DIGIT
163
+ # ; Unsigned 32-bit integer
164
+ # ; (0 <= n < 4,294,967,296)
161
165
  def valid_number?(num)
162
- # [RFC 3501]
163
- # number = 1*DIGIT
164
- # ; Unsigned 32-bit integer
165
- # ; (0 <= n < 4,294,967,296)
166
- num >= 0 && num < 4294967296
166
+ 0 <= num && num <= 0xffff_ffff
167
167
  end
168
168
 
169
- # Check is passed argument valid 'nz_number' in RFC 3501 terminology
169
+ # Check if argument is a valid 'nz-number' according to RFC 3501
170
+ # nz-number = digit-nz *DIGIT
171
+ # ; Non-zero unsigned 32-bit integer
172
+ # ; (0 < n < 4,294,967,296)
170
173
  def valid_nz_number?(num)
171
- # [RFC 3501]
172
- # nz-number = digit-nz *DIGIT
173
- # ; Non-zero unsigned 32-bit integer
174
- # ; (0 < n < 4,294,967,296)
175
- num != 0 && valid_number?(num)
174
+ 0 < num && num <= 0xffff_ffff
176
175
  end
177
176
 
178
- # Check is passed argument valid 'mod_sequence_value' in RFC 4551 terminology
177
+ # Check if argument is a valid 'mod-sequence-value' according to RFC 4551
178
+ # mod-sequence-value = 1*DIGIT
179
+ # ; Positive unsigned 64-bit integer
180
+ # ; (mod-sequence)
181
+ # ; (1 <= n < 18,446,744,073,709,551,615)
179
182
  def valid_mod_sequence_value?(num)
180
- # mod-sequence-value = 1*DIGIT
181
- # ; Positive unsigned 64-bit integer
182
- # ; (mod-sequence)
183
- # ; (1 <= n < 18,446,744,073,709,551,615)
184
- num >= 1 && num < 18446744073709551615
183
+ 1 <= num && num < 0xffff_ffff_ffff_ffff
184
+ end
185
+
186
+ # Check if argument is a valid 'mod-sequence-valzer' according to RFC 4551
187
+ # mod-sequence-valzer = "0" / mod-sequence-value
188
+ def valid_mod_sequence_valzer?(num)
189
+ 0 <= num && num < 0xffff_ffff_ffff_ffff
185
190
  end
186
191
 
187
192
  # Ensure argument is 'number' or raise DataFormatError
188
193
  def ensure_number(num)
189
- return if valid_number?(num)
190
-
191
- msg = "number must be unsigned 32-bit integer: #{num}"
192
- raise DataFormatError, msg
194
+ return num if valid_number?(num)
195
+ raise DataFormatError,
196
+ "number must be unsigned 32-bit integer: #{num}"
193
197
  end
194
198
 
195
- # Ensure argument is 'nz_number' or raise DataFormatError
199
+ # Ensure argument is 'nz-number' or raise DataFormatError
196
200
  def ensure_nz_number(num)
197
- return if valid_nz_number?(num)
198
-
199
- msg = "nz_number must be non-zero unsigned 32-bit integer: #{num}"
200
- raise DataFormatError, msg
201
+ return num if valid_nz_number?(num)
202
+ raise DataFormatError,
203
+ "nz-number must be non-zero unsigned 32-bit integer: #{num}"
201
204
  end
202
205
 
203
- # Ensure argument is 'mod_sequence_value' or raise DataFormatError
206
+ # Ensure argument is 'mod-sequence-value' or raise DataFormatError
204
207
  def ensure_mod_sequence_value(num)
205
- return if valid_mod_sequence_value?(num)
208
+ return num if valid_mod_sequence_value?(num)
209
+ raise DataFormatError,
210
+ "mod-sequence-value must be non-zero unsigned 64-bit integer: #{num}"
211
+ end
212
+
213
+ # Ensure argument is 'mod-sequence-valzer' or raise DataFormatError
214
+ def ensure_mod_sequence_valzer(num)
215
+ return num if valid_mod_sequence_valzer?(num)
216
+ raise DataFormatError,
217
+ "mod-sequence-valzer must be unsigned 64-bit integer: #{num}"
218
+ end
219
+
220
+ # Like #ensure_number, but usable with numeric String input.
221
+ def coerce_number(num)
222
+ case num
223
+ when Integer then ensure_number num
224
+ when NUMBER_RE then ensure_number Integer num
225
+ else
226
+ raise DataFormatError, "%p is not a valid number" % [num]
227
+ end
228
+ end
206
229
 
207
- msg = "mod_sequence_value must be unsigned 64-bit integer: #{num}"
208
- raise DataFormatError, msg
230
+ # Like #ensure_nz_number, but usable with numeric String input.
231
+ def coerce_nz_number(num)
232
+ case num
233
+ when Integer then ensure_nz_number num
234
+ when NUMBER_RE then ensure_nz_number Integer num
235
+ else
236
+ raise DataFormatError, "%p is not a valid nz-number" % [num]
237
+ end
238
+ end
239
+
240
+ # Like #ensure_mod_sequence_value, but usable with numeric String input.
241
+ def coerce_mod_sequence_value(num)
242
+ case num
243
+ when Integer then ensure_mod_sequence_value num
244
+ when NUMBER_RE then ensure_mod_sequence_value Integer num
245
+ else
246
+ raise DataFormatError, "%p is not a valid mod-sequence-value" % [num]
247
+ end
248
+ end
249
+
250
+ # Like #ensure_mod_sequence_valzer, but usable with numeric String input.
251
+ def coerce_mod_sequence_valzer(num)
252
+ case num
253
+ when Integer then ensure_mod_sequence_valzer num
254
+ when NUMBER_RE then ensure_mod_sequence_valzer Integer num
255
+ else
256
+ raise DataFormatError, "%p is not a valid mod-sequence-valzer" % [num]
257
+ end
209
258
  end
210
259
 
211
260
  end
@@ -83,10 +83,12 @@ module Net
83
83
  elsif deprecated.empty?
84
84
  super host, port: port_or_options
85
85
  elsif deprecated.shift
86
- warn "DEPRECATED: Call Net::IMAP.new with keyword options", uplevel: 1
86
+ warn("DEPRECATED: Call Net::IMAP.new with keyword options",
87
+ uplevel: 1, category: :deprecated)
87
88
  super host, port: port_or_options, ssl: create_ssl_params(*deprecated)
88
89
  else
89
- warn "DEPRECATED: Call Net::IMAP.new with keyword options", uplevel: 1
90
+ warn("DEPRECATED: Call Net::IMAP.new with keyword options",
91
+ uplevel: 1, category: :deprecated)
90
92
  super host, port: port_or_options, ssl: false
91
93
  end
92
94
  end
@@ -113,7 +115,8 @@ module Net
113
115
  elsif deprecated.first.respond_to?(:to_hash)
114
116
  super(**Hash.try_convert(deprecated.first))
115
117
  else
116
- warn "DEPRECATED: Call Net::IMAP#starttls with keyword options", uplevel: 1
118
+ warn("DEPRECATED: Call Net::IMAP#starttls with keyword options",
119
+ uplevel: 1, category: :deprecated)
117
120
  super(**create_ssl_params(*deprecated))
118
121
  end
119
122
  end
@@ -7,6 +7,12 @@ module Net
7
7
  class Error < StandardError
8
8
  end
9
9
 
10
+ class LoginDisabledError < Error
11
+ def initialize(msg = "Remote server has disabled the LOGIN command", ...)
12
+ super
13
+ end
14
+ end
15
+
10
16
  # Error raised when data is in the incorrect format.
11
17
  class DataFormatError < Error
12
18
  end
@@ -45,7 +51,159 @@ module Net
45
51
  end
46
52
 
47
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.
48
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
+ backtrace_locations&.each_with_index do |loc, idx|
176
+ next if loc.base_label.include? "parse_error"
177
+ break if loc.base_label == "parse"
178
+ if loc.label.include?("#") # => Class#method, since ruby 3.4
179
+ next unless loc.label&.include?(parser_class.name)
180
+ else
181
+ next unless loc.path&.include?("net/imap/response_parser")
182
+ end
183
+ msg << "\n %s: %s (%s:%d)" % [
184
+ hl["%{key}caller[%{/key}%{idx}%%2d%{/idx}%{key}]%{/key}"] % idx,
185
+ hl["%{label}%%-30s%{/label}"] % loc.base_label,
186
+ File.basename(loc.path, ".rb"), loc.lineno
187
+ ]
188
+ end
189
+ end
190
+ msg
191
+ rescue => error
192
+ msg ||= super.dup
193
+ msg << "\n BUG in %s#%s: %s" % [self.class, __method__,
194
+ error.detailed_message]
195
+ msg
196
+ end
197
+
198
+ def processed_string = string && pos && string[...pos]
199
+ def remaining_string = string && pos && string[pos..]
200
+
201
+ private
202
+
203
+ def default_highlight_from_env
204
+ (ENV["FORCE_COLOR"] || "") !~ /\A(?:0|)\z/ ||
205
+ (ENV["TERM"] || "") !~ /\A(?:dumb|unknown|)\z/i
206
+ end
49
207
  end
50
208
 
51
209
  # Superclass of all errors used to encapsulate "fail" responses
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+ # An "extended search" response (+ESEARCH+). ESearchResult should be
6
+ # returned (instead of SearchResult) by IMAP#search, IMAP#uid_search,
7
+ # IMAP#sort, and IMAP#uid_sort under any of the following conditions:
8
+ #
9
+ # * Return options were specified for IMAP#search or IMAP#uid_search.
10
+ # The server must support a search extension which allows
11
+ # RFC4466[https://www.rfc-editor.org/rfc/rfc4466.html] +return+ options,
12
+ # such as +ESEARCH+, +PARTIAL+, or +IMAP4rev2+.
13
+ # * Return options were specified for IMAP#sort or IMAP#uid_sort.
14
+ # The server must support the +ESORT+ extension
15
+ # {[RFC5267]}[https://www.rfc-editor.org/rfc/rfc5267.html#section-3].
16
+ #
17
+ # *NOTE:* IMAP#search and IMAP#uid_search do not support +ESORT+ yet.
18
+ # * The server supports +IMAP4rev2+ but _not_ +IMAP4rev1+, or +IMAP4rev2+
19
+ # has been enabled. +IMAP4rev2+ requires +ESEARCH+ results.
20
+ #
21
+ # Note that some servers may claim to support a search extension which
22
+ # requires an +ESEARCH+ result, such as +PARTIAL+, but still only return a
23
+ # +SEARCH+ result when +return+ options are specified.
24
+ #
25
+ # Some search extensions may result in the server sending ESearchResult
26
+ # responses after the initiating command has completed. Use
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).
34
+ class ESearchResult < Data.define(:tag, :uid, :data)
35
+ def initialize(tag: nil, uid: nil, data: nil)
36
+ tag => String | nil; tag = -tag if tag
37
+ uid => true | false | nil; uid = !!uid
38
+ data => Array | nil; data ||= []; data.freeze
39
+ super
40
+ end
41
+
42
+ # :call-seq: to_a -> Array of integers
43
+ #
44
+ # When either #all or #partial contains a SequenceSet of message sequence
45
+ # numbers or UIDs, +to_a+ returns that set as an array of integers.
46
+ #
47
+ # When both #all and #partial are +nil+, either because the server
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.
50
+ #
51
+ # Note that SearchResult also implements +to_a+, so it can be used without
52
+ # checking if the server returned +SEARCH+ or +ESEARCH+ data.
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
91
+
92
+ ##
93
+ # attr_reader: tag
94
+ #
95
+ # The tag string for the command that caused this response to be returned.
96
+ #
97
+ # When +nil+, this response was not caused by a particular command.
98
+
99
+ ##
100
+ # attr_reader: uid
101
+ #
102
+ # Indicates whether #data in this response refers to UIDs (when +true+) or
103
+ # to message sequence numbers (when +false+).
104
+
105
+ ##
106
+ alias uid? uid
107
+
108
+ ##
109
+ # attr_reader: data
110
+ #
111
+ # Search return data, as an array of <tt>[name, value]</tt> pairs. Most
112
+ # return data corresponds to a search +return+ option with the same name.
113
+ #
114
+ # Note that some return data names may be used more than once per result.
115
+ #
116
+ # This data can be more simply retrieved by #min, #max, #all, #count,
117
+ # #modseq, and other methods.
118
+
119
+ # :call-seq: min -> integer or nil
120
+ #
121
+ # The lowest message number/UID that satisfies the SEARCH criteria.
122
+ #
123
+ # Returns +nil+ when the associated search command has no results, or when
124
+ # the +MIN+ return option wasn't specified.
125
+ #
126
+ # Requires +ESEARCH+ {[RFC4731]}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.1] or
127
+ # +IMAP4rev2+ {[RFC9051]}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.3.4].
128
+ def min; data.assoc("MIN")&.last end
129
+
130
+ # :call-seq: max -> integer or nil
131
+ #
132
+ # The highest message number/UID that satisfies the SEARCH criteria.
133
+ #
134
+ # Returns +nil+ when the associated search command has no results, or when
135
+ # the +MAX+ return option wasn't specified.
136
+ #
137
+ # Requires +ESEARCH+ {[RFC4731]}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.1] or
138
+ # +IMAP4rev2+ {[RFC9051]}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.3.4].
139
+ def max; data.assoc("MAX")&.last end
140
+
141
+ # :call-seq: all -> sequence set or nil
142
+ #
143
+ # A SequenceSet containing all message sequence numbers or UIDs that
144
+ # satisfy the SEARCH criteria.
145
+ #
146
+ # Returns +nil+ when the associated search command has no results, or when
147
+ # the +ALL+ return option was not specified but other return options were.
148
+ #
149
+ # Requires +ESEARCH+ {[RFC4731]}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.1] or
150
+ # +IMAP4rev2+ {[RFC9051]}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.3.4].
151
+ #
152
+ # See also: #to_a
153
+ def all; data.assoc("ALL")&.last end
154
+
155
+ # :call-seq: count -> integer or nil
156
+ #
157
+ # Returns the number of messages that satisfy the SEARCH criteria.
158
+ #
159
+ # Returns +nil+ when the associated search command has no results, or when
160
+ # the +COUNT+ return option wasn't specified.
161
+ #
162
+ # Requires +ESEARCH+ {[RFC4731]}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.1] or
163
+ # +IMAP4rev2+ {[RFC9051]}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.3.4].
164
+ def count; data.assoc("COUNT")&.last end
165
+
166
+ # :call-seq: modseq -> integer or nil
167
+ #
168
+ # The highest +mod-sequence+ of all messages being returned.
169
+ #
170
+ # Returns +nil+ when the associated search command has no results, or when
171
+ # the +MODSEQ+ search criterion wasn't specified.
172
+ #
173
+ # Note that there is no search +return+ option for +MODSEQ+. It will be
174
+ # returned whenever the +CONDSTORE+ extension has been enabled. Using the
175
+ # +MODSEQ+ search criteria will implicitly enable +CONDSTORE+.
176
+ #
177
+ # Requires +CONDSTORE+ {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html]
178
+ # and +ESEARCH+ {[RFC4731]}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.2].
179
+ def modseq; data.assoc("MODSEQ")&.last end
180
+
181
+ # Returned by ESearchResult#partial.
182
+ #
183
+ # Requires +PARTIAL+ {[RFC9394]}[https://www.rfc-editor.org/rfc/rfc9394.html]
184
+ # or <tt>CONTEXT=SEARCH</tt>/<tt>CONTEXT=SORT</tt>
185
+ # {[RFC5267]}[https://www.rfc-editor.org/rfc/rfc5267.html]
186
+ #
187
+ # See also: #to_a
188
+ class PartialResult < Data.define(:range, :results)
189
+ def initialize(range:, results:)
190
+ range => Range
191
+ results = SequenceSet[results] unless results.nil?
192
+ super
193
+ end
194
+
195
+ ##
196
+ # method: range
197
+ # :call-seq: range -> range
198
+
199
+ ##
200
+ # method: results
201
+ # :call-seq: results -> sequence set or nil
202
+
203
+ # Converts #results to an array of integers.
204
+ #
205
+ # See also: ESearchResult#to_a.
206
+ def to_a; results&.numbers || [] end
207
+
208
+ alias to_sequence_set results
209
+ end
210
+
211
+ # :call-seq: partial -> PartialResult or nil
212
+ #
213
+ # A PartialResult containing a subset of the message sequence numbers or
214
+ # UIDs that satisfy the SEARCH criteria.
215
+ #
216
+ # Requires +PARTIAL+ {[RFC9394]}[https://www.rfc-editor.org/rfc/rfc9394.html]
217
+ # or <tt>CONTEXT=SEARCH</tt>/<tt>CONTEXT=SORT</tt>
218
+ # {[RFC5267]}[https://www.rfc-editor.org/rfc/rfc5267.html]
219
+ #
220
+ # See also: #to_a
221
+ def partial; data.assoc("PARTIAL")&.last end
222
+
223
+ end
224
+ end
225
+ end