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.
- checksums.yaml +4 -4
- data/Gemfile +12 -2
- data/README.md +10 -4
- data/docs/styles.css +75 -14
- data/lib/net/imap/authenticators.rb +2 -2
- data/lib/net/imap/command_data.rb +40 -95
- 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 +22 -10
- data/lib/net/imap/config/attr_version_defaults.rb +90 -0
- data/lib/net/imap/config.rb +241 -125
- data/lib/net/imap/connection_state.rb +48 -0
- data/lib/net/imap/data_encoding.rb +80 -31
- data/lib/net/imap/deprecated_client_options.rb +6 -3
- data/lib/net/imap/errors.rb +158 -0
- data/lib/net/imap/esearch_result.rb +225 -0
- data/lib/net/imap/fetch_data.rb +126 -47
- data/lib/net/imap/flags.rb +1 -1
- data/lib/net/imap/response_data.rb +123 -187
- data/lib/net/imap/response_parser/parser_utils.rb +19 -23
- data/lib/net/imap/response_parser.rb +182 -38
- data/lib/net/imap/response_reader.rb +10 -12
- data/lib/net/imap/sasl/anonymous_authenticator.rb +3 -3
- data/lib/net/imap/sasl/authentication_exchange.rb +52 -20
- data/lib/net/imap/sasl/authenticators.rb +8 -4
- data/lib/net/imap/sasl/client_adapter.rb +77 -26
- data/lib/net/imap/sasl/cram_md5_authenticator.rb +4 -4
- data/lib/net/imap/sasl/digest_md5_authenticator.rb +218 -56
- data/lib/net/imap/sasl/external_authenticator.rb +2 -2
- data/lib/net/imap/sasl/gs2_header.rb +7 -7
- data/lib/net/imap/sasl/login_authenticator.rb +4 -3
- data/lib/net/imap/sasl/oauthbearer_authenticator.rb +6 -6
- data/lib/net/imap/sasl/plain_authenticator.rb +7 -7
- data/lib/net/imap/sasl/protocol_adapters.rb +60 -4
- data/lib/net/imap/sasl/scram_authenticator.rb +8 -8
- data/lib/net/imap/sasl.rb +7 -4
- data/lib/net/imap/sasl_adapter.rb +0 -1
- data/lib/net/imap/search_result.rb +10 -5
- data/lib/net/imap/sequence_set.rb +1104 -421
- data/lib/net/imap/stringprep/nameprep.rb +1 -1
- data/lib/net/imap/stringprep/trace.rb +4 -4
- data/lib/net/imap/uidplus_data.rb +4 -147
- data/lib/net/imap/vanished_data.rb +65 -0
- data/lib/net/imap.rb +1002 -313
- data/net-imap.gemspec +1 -1
- data/rakelib/rfcs.rake +2 -0
- data/rakelib/string_prep_tables_generator.rb +6 -2
- 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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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 '
|
|
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
|
-
|
|
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 '
|
|
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
|
-
|
|
208
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
data/lib/net/imap/errors.rb
CHANGED
|
@@ -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
|