net-imap 0.3.4 → 0.5.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/BSDL +22 -0
  3. data/COPYING +56 -0
  4. data/Gemfile +14 -0
  5. data/LICENSE.txt +3 -22
  6. data/README.md +25 -8
  7. data/Rakefile +0 -7
  8. data/docs/styles.css +72 -23
  9. data/lib/net/imap/authenticators.rb +26 -57
  10. data/lib/net/imap/command_data.rb +74 -54
  11. data/lib/net/imap/config/attr_accessors.rb +75 -0
  12. data/lib/net/imap/config/attr_inheritance.rb +90 -0
  13. data/lib/net/imap/config/attr_type_coercion.rb +61 -0
  14. data/lib/net/imap/config.rb +470 -0
  15. data/lib/net/imap/data_encoding.rb +21 -9
  16. data/lib/net/imap/data_lite.rb +226 -0
  17. data/lib/net/imap/deprecated_client_options.rb +142 -0
  18. data/lib/net/imap/errors.rb +27 -1
  19. data/lib/net/imap/esearch_result.rb +180 -0
  20. data/lib/net/imap/fetch_data.rb +597 -0
  21. data/lib/net/imap/flags.rb +1 -1
  22. data/lib/net/imap/response_data.rb +250 -440
  23. data/lib/net/imap/response_parser/parser_utils.rb +245 -0
  24. data/lib/net/imap/response_parser.rb +1867 -1184
  25. data/lib/net/imap/sasl/anonymous_authenticator.rb +69 -0
  26. data/lib/net/imap/sasl/authentication_exchange.rb +139 -0
  27. data/lib/net/imap/sasl/authenticators.rb +122 -0
  28. data/lib/net/imap/sasl/client_adapter.rb +123 -0
  29. data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +24 -14
  30. data/lib/net/imap/sasl/digest_md5_authenticator.rb +342 -0
  31. data/lib/net/imap/sasl/external_authenticator.rb +83 -0
  32. data/lib/net/imap/sasl/gs2_header.rb +80 -0
  33. data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +28 -18
  34. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +199 -0
  35. data/lib/net/imap/sasl/plain_authenticator.rb +101 -0
  36. data/lib/net/imap/sasl/protocol_adapters.rb +101 -0
  37. data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
  38. data/lib/net/imap/sasl/scram_authenticator.rb +287 -0
  39. data/lib/net/imap/sasl/stringprep.rb +6 -66
  40. data/lib/net/imap/sasl/xoauth2_authenticator.rb +106 -0
  41. data/lib/net/imap/sasl.rb +148 -44
  42. data/lib/net/imap/sasl_adapter.rb +20 -0
  43. data/lib/net/imap/search_result.rb +146 -0
  44. data/lib/net/imap/sequence_set.rb +1565 -0
  45. data/lib/net/imap/stringprep/nameprep.rb +70 -0
  46. data/lib/net/imap/stringprep/saslprep.rb +69 -0
  47. data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
  48. data/lib/net/imap/stringprep/tables.rb +146 -0
  49. data/lib/net/imap/stringprep/trace.rb +85 -0
  50. data/lib/net/imap/stringprep.rb +159 -0
  51. data/lib/net/imap/uidplus_data.rb +244 -0
  52. data/lib/net/imap/vanished_data.rb +56 -0
  53. data/lib/net/imap.rb +2090 -823
  54. data/net-imap.gemspec +7 -8
  55. data/rakelib/benchmarks.rake +91 -0
  56. data/rakelib/rfcs.rake +2 -0
  57. data/rakelib/saslprep.rake +4 -4
  58. data/rakelib/string_prep_tables_generator.rb +84 -60
  59. data/sample/net-imap.rb +167 -0
  60. metadata +45 -49
  61. data/.github/dependabot.yml +0 -6
  62. data/.github/workflows/test.yml +0 -31
  63. data/.gitignore +0 -10
  64. data/benchmarks/stringprep.yml +0 -65
  65. data/benchmarks/table-regexps.yml +0 -39
  66. data/lib/net/imap/authenticators/digest_md5.rb +0 -115
  67. data/lib/net/imap/authenticators/plain.rb +0 -41
  68. data/lib/net/imap/authenticators/xoauth2.rb +0 -20
  69. data/lib/net/imap/sasl/saslprep.rb +0 -55
  70. data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
  71. data/lib/net/imap/sasl/stringprep_tables.rb +0 -153
@@ -0,0 +1,1565 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set" unless defined?(::Set)
4
+
5
+ module Net
6
+ class IMAP
7
+
8
+ ##
9
+ # An \IMAP sequence set is a set of message sequence numbers or unique
10
+ # identifier numbers ("UIDs"). It contains numbers and ranges of numbers.
11
+ # The numbers are all non-zero unsigned 32-bit integers and one special
12
+ # value (<tt>"*"</tt>) that represents the largest value in the mailbox.
13
+ #
14
+ # Certain types of \IMAP responses will contain a SequenceSet, for example
15
+ # the data for a <tt>"MODIFIED"</tt> ResponseCode. Some \IMAP commands may
16
+ # receive a SequenceSet as an argument, for example IMAP#search, IMAP#fetch,
17
+ # and IMAP#store.
18
+ #
19
+ # == Creating sequence sets
20
+ #
21
+ # SequenceSet.new with no arguments creates an empty sequence set. Note
22
+ # that an empty sequence set is invalid in the \IMAP grammar.
23
+ #
24
+ # set = Net::IMAP::SequenceSet.new
25
+ # set.empty? #=> true
26
+ # set.valid? #=> false
27
+ # set.valid_string #!> raises DataFormatError
28
+ # set << 1..10
29
+ # set.empty? #=> false
30
+ # set.valid? #=> true
31
+ # set.valid_string #=> "1:10"
32
+ #
33
+ # SequenceSet.new may receive a single optional argument: a non-zero 32 bit
34
+ # unsigned integer, a range, a <tt>sequence-set</tt> formatted string,
35
+ # another sequence set, a Set (containing only numbers or <tt>*</tt>), or an
36
+ # Array containing any of these (array inputs may be nested).
37
+ #
38
+ # set = Net::IMAP::SequenceSet.new(1)
39
+ # set.valid_string #=> "1"
40
+ # set = Net::IMAP::SequenceSet.new(1..100)
41
+ # set.valid_string #=> "1:100"
42
+ # set = Net::IMAP::SequenceSet.new(1...100)
43
+ # set.valid_string #=> "1:99"
44
+ # set = Net::IMAP::SequenceSet.new([1, 2, 5..])
45
+ # set.valid_string #=> "1:2,5:*"
46
+ # set = Net::IMAP::SequenceSet.new("1,2,3:7,5,6:10,2048,1024")
47
+ # set.valid_string #=> "1,2,3:7,5,6:10,2048,1024"
48
+ # set = Net::IMAP::SequenceSet.new(1, 2, 3..7, 5, 6..10, 2048, 1024)
49
+ # set.valid_string #=> "1:10,55,1024:2048"
50
+ #
51
+ # Use ::[] with one or more arguments to create a frozen SequenceSet. An
52
+ # invalid (empty) set cannot be created with ::[].
53
+ #
54
+ # set = Net::IMAP::SequenceSet["1,2,3:7,5,6:10,2048,1024"]
55
+ # set.valid_string #=> "1,2,3:7,5,6:10,2048,1024"
56
+ # set = Net::IMAP::SequenceSet[1, 2, [3..7, 5], 6..10, 2048, 1024]
57
+ # set.valid_string #=> "1:10,55,1024:2048"
58
+ #
59
+ # == Ordered and Normalized sets
60
+ #
61
+ # Sometimes the order of the set's members is significant, such as with the
62
+ # +ESORT+, <tt>CONTEXT=SORT</tt>, and +UIDPLUS+ extensions. So, when a
63
+ # sequence set is created by the parser or with a single string value, that
64
+ # #string representation is preserved.
65
+ #
66
+ # Internally, SequenceSet stores a normalized representation which sorts all
67
+ # entries, de-duplicates numbers, and coalesces adjacent or overlapping
68
+ # ranges. Most methods use this normalized representation to achieve
69
+ # <tt>O(lg n)</tt> porformance. Use #entries or #each_entry to enumerate
70
+ # the set in its original order.
71
+ #
72
+ # Most modification methods convert #string to its normalized form. To
73
+ # preserve #string order while modifying a set, use #append, #string=, or
74
+ # #replace.
75
+ #
76
+ # == Using <tt>*</tt>
77
+ #
78
+ # \IMAP sequence sets may contain a special value <tt>"*"</tt>, which
79
+ # represents the largest number in use. From +seq-number+ in
80
+ # {RFC9051 §9}[https://www.rfc-editor.org/rfc/rfc9051.html#section-9-5]:
81
+ # >>>
82
+ # In the case of message sequence numbers, it is the number of messages
83
+ # in a non-empty mailbox. In the case of unique identifiers, it is the
84
+ # unique identifier of the last message in the mailbox or, if the
85
+ # mailbox is empty, the mailbox's current UIDNEXT value.
86
+ #
87
+ # When creating a SequenceSet, <tt>*</tt> may be input as <tt>-1</tt>,
88
+ # <tt>"*"</tt>, <tt>:*</tt>, an endless range, or a range ending in
89
+ # <tt>-1</tt>. When converting to #elements, #ranges, or #numbers, it will
90
+ # output as either <tt>:*</tt> or an endless range. For example:
91
+ #
92
+ # Net::IMAP::SequenceSet["1,3,*"].to_a #=> [1, 3, :*]
93
+ # Net::IMAP::SequenceSet["1,234:*"].to_a #=> [1, 234..]
94
+ # Net::IMAP::SequenceSet[1234..-1].to_a #=> [1234..]
95
+ # Net::IMAP::SequenceSet[1234..].to_a #=> [1234..]
96
+ #
97
+ # Net::IMAP::SequenceSet[1234..].to_s #=> "1234:*"
98
+ # Net::IMAP::SequenceSet[1234..-1].to_s #=> "1234:*"
99
+ #
100
+ # Use #limit to convert <tt>"*"</tt> to a maximum value. When a range
101
+ # includes <tt>"*"</tt>, the maximum value will always be matched:
102
+ #
103
+ # Net::IMAP::SequenceSet["9999:*"].limit(max: 25)
104
+ # #=> Net::IMAP::SequenceSet["25"]
105
+ #
106
+ # === Surprising <tt>*</tt> behavior
107
+ #
108
+ # When a set includes <tt>*</tt>, some methods may have surprising behavior.
109
+ #
110
+ # For example, #complement treats <tt>*</tt> as its own number. This way,
111
+ # the #intersection of a set and its #complement will always be empty.
112
+ # This is not how an \IMAP server interprets the set: it will convert
113
+ # <tt>*</tt> to either the number of messages in the mailbox or +UIDNEXT+,
114
+ # as appropriate. And there _will_ be overlap between a set and its
115
+ # complement after #limit is applied to each:
116
+ #
117
+ # ~Net::IMAP::SequenceSet["*"] == Net::IMAP::SequenceSet[1..(2**32-1)]
118
+ # ~Net::IMAP::SequenceSet[1..5] == Net::IMAP::SequenceSet["6:*"]
119
+ #
120
+ # set = Net::IMAP::SequenceSet[1..5]
121
+ # (set & ~set).empty? => true
122
+ #
123
+ # (set.limit(max: 4) & (~set).limit(max: 4)).to_a => [4]
124
+ #
125
+ # When counting the number of numbers in a set, <tt>*</tt> will be counted
126
+ # _except_ when UINT32_MAX is also in the set:
127
+ # UINT32_MAX = 2**32 - 1
128
+ # Net::IMAP::SequenceSet["*"].count => 1
129
+ # Net::IMAP::SequenceSet[1..UINT32_MAX - 1, :*].count => UINT32_MAX
130
+ #
131
+ # Net::IMAP::SequenceSet["1:*"].count => UINT32_MAX
132
+ # Net::IMAP::SequenceSet[UINT32_MAX, :*].count => 1
133
+ # Net::IMAP::SequenceSet[UINT32_MAX..].count => 1
134
+ #
135
+ # == What's here?
136
+ #
137
+ # SequenceSet provides methods for:
138
+ # * {Creating a SequenceSet}[rdoc-ref:SequenceSet@Methods+for+Creating+a+SequenceSet]
139
+ # * {Comparing}[rdoc-ref:SequenceSet@Methods+for+Comparing]
140
+ # * {Querying}[rdoc-ref:SequenceSet@Methods+for+Querying]
141
+ # * {Iterating}[rdoc-ref:SequenceSet@Methods+for+Iterating]
142
+ # * {Set Operations}[rdoc-ref:SequenceSet@Methods+for+Set+Operations]
143
+ # * {Assigning}[rdoc-ref:SequenceSet@Methods+for+Assigning]
144
+ # * {Deleting}[rdoc-ref:SequenceSet@Methods+for+Deleting]
145
+ # * {IMAP String Formatting}[rdoc-ref:SequenceSet@Methods+for+IMAP+String+Formatting]
146
+ #
147
+ # === Methods for Creating a \SequenceSet
148
+ # * ::[]: Creates a validated frozen sequence set from one or more inputs.
149
+ # * ::new: Creates a new mutable sequence set, which may be empty (invalid).
150
+ # * ::try_convert: Calls +to_sequence_set+ on an object and verifies that
151
+ # the result is a SequenceSet.
152
+ # * ::empty: Returns a frozen empty (invalid) SequenceSet.
153
+ # * ::full: Returns a frozen SequenceSet containing every possible number.
154
+ #
155
+ # === Methods for Comparing
156
+ #
157
+ # <i>Comparison to another \SequenceSet:</i>
158
+ # - #==: Returns whether a given set contains the same numbers as +self+.
159
+ # - #eql?: Returns whether a given set uses the same #string as +self+.
160
+ #
161
+ # <i>Comparison to objects which are convertible to \SequenceSet:</i>
162
+ # - #===:
163
+ # Returns whether a given object is fully contained within +self+, or
164
+ # +nil+ if the object cannot be converted to a compatible type.
165
+ # - #cover?:
166
+ # Returns whether a given object is fully contained within +self+.
167
+ # - #intersect? (aliased as #overlap?):
168
+ # Returns whether +self+ and a given object have any common elements.
169
+ # - #disjoint?:
170
+ # Returns whether +self+ and a given object have no common elements.
171
+ #
172
+ # === Methods for Querying
173
+ # These methods do not modify +self+.
174
+ #
175
+ # <i>Set membership:</i>
176
+ # - #include? (aliased as #member?):
177
+ # Returns whether a given object (nz-number, range, or <tt>*</tt>) is
178
+ # contained by the set.
179
+ # - #include_star?: Returns whether the set contains <tt>*</tt>.
180
+ #
181
+ # <i>Minimum and maximum value elements:</i>
182
+ # - #min: Returns the minimum number in the set.
183
+ # - #max: Returns the maximum number in the set.
184
+ # - #minmax: Returns the minimum and maximum numbers in the set.
185
+ #
186
+ # <i>Accessing value by offset in sorted set:</i>
187
+ # - #[] (aliased as #slice): Returns the number or consecutive subset at a
188
+ # given offset or range of offsets in the sorted set.
189
+ # - #at: Returns the number at a given offset in the sorted set.
190
+ # - #find_index: Returns the given number's offset in the sorted set.
191
+ #
192
+ # <i>Accessing value by offset in ordered entries</i>
193
+ # - #ordered_at: Returns the number at a given offset in the ordered entries.
194
+ # - #find_ordered_index: Returns the index of the given number's first
195
+ # occurrence in entries.
196
+ #
197
+ # <i>Set cardinality:</i>
198
+ # - #count (aliased as #size): Returns the count of numbers in the set.
199
+ # Duplicated numbers are not counted.
200
+ # - #empty?: Returns whether the set has no members. \IMAP syntax does not
201
+ # allow empty sequence sets.
202
+ # - #valid?: Returns whether the set has any members.
203
+ # - #full?: Returns whether the set contains every possible value, including
204
+ # <tt>*</tt>.
205
+ #
206
+ # <i>Denormalized properties:</i>
207
+ # - #has_duplicates?: Returns whether the ordered entries repeat any
208
+ # numbers.
209
+ # - #count_duplicates: Returns the count of repeated numbers in the ordered
210
+ # entries.
211
+ # - #count_with_duplicates: Returns the count of numbers in the ordered
212
+ # entries, including any repeated numbers.
213
+ #
214
+ # === Methods for Iterating
215
+ #
216
+ # <i>Normalized (sorted and coalesced):</i>
217
+ # - #each_element: Yields each number and range in the set, sorted and
218
+ # coalesced, and returns +self+.
219
+ # - #elements (aliased as #to_a): Returns an Array of every number and range
220
+ # in the set, sorted and coalesced.
221
+ # - #each_range:
222
+ # Yields each element in the set as a Range and returns +self+.
223
+ # - #ranges: Returns an Array of every element in the set, converting
224
+ # numbers into ranges of a single value.
225
+ # - #each_number: Yields each number in the set and returns +self+.
226
+ # - #numbers: Returns an Array with every number in the set, expanding
227
+ # ranges into all of their contained numbers.
228
+ # - #to_set: Returns a Set containing all of the #numbers in the set.
229
+ #
230
+ # <i>Order preserving:</i>
231
+ # - #each_entry: Yields each number and range in the set, unsorted and
232
+ # without deduplicating numbers or coalescing ranges, and returns +self+.
233
+ # - #entries: Returns an Array of every number and range in the set,
234
+ # unsorted and without deduplicating numbers or coalescing ranges.
235
+ # - #each_ordered_number: Yields each number in the ordered entries and
236
+ # returns +self+.
237
+ #
238
+ # === Methods for \Set Operations
239
+ # These methods do not modify +self+.
240
+ #
241
+ # - #| (aliased as #union and #+): Returns a new set combining all members
242
+ # from +self+ with all members from the other object.
243
+ # - #& (aliased as #intersection): Returns a new set containing all members
244
+ # common to +self+ and the other object.
245
+ # - #- (aliased as #difference): Returns a copy of +self+ with all members
246
+ # in the other object removed.
247
+ # - #^ (aliased as #xor): Returns a new set containing all members from
248
+ # +self+ and the other object except those common to both.
249
+ # - #~ (aliased as #complement): Returns a new set containing all members
250
+ # that are not in +self+
251
+ # - #limit: Returns a copy of +self+ which has replaced <tt>*</tt> with a
252
+ # given maximum value and removed all members over that maximum.
253
+ #
254
+ # === Methods for Assigning
255
+ # These methods add or replace elements in +self+.
256
+ #
257
+ # <i>Normalized (sorted and coalesced):</i>
258
+ #
259
+ # These methods always update #string to be fully sorted and coalesced.
260
+ #
261
+ # - #add (aliased as #<<): Adds a given object to the set; returns +self+.
262
+ # - #add?: If the given object is not an element in the set, adds it and
263
+ # returns +self+; otherwise, returns +nil+.
264
+ # - #merge: Merges multiple elements into the set; returns +self+.
265
+ # - #complement!: Replaces the contents of the set with its own #complement.
266
+ #
267
+ # <i>Order preserving:</i>
268
+ #
269
+ # These methods _may_ cause #string to not be sorted or coalesced.
270
+ #
271
+ # - #append: Adds a given object to the set, appending it to the existing
272
+ # string, and returns +self+.
273
+ # - #string=: Assigns a new #string value and replaces #elements to match.
274
+ # - #replace: Replaces the contents of the set with the contents
275
+ # of a given object.
276
+ #
277
+ # === Methods for Deleting
278
+ # These methods remove elements from +self+, and update #string to be fully
279
+ # sorted and coalesced.
280
+ #
281
+ # - #clear: Removes all elements in the set; returns +self+.
282
+ # - #delete: Removes a given object from the set; returns +self+.
283
+ # - #delete?: If the given object is an element in the set, removes it and
284
+ # returns it; otherwise, returns +nil+.
285
+ # - #delete_at: Removes the number at a given offset.
286
+ # - #slice!: Removes the number or consecutive numbers at a given offset or
287
+ # range of offsets.
288
+ # - #subtract: Removes each given object from the set; returns +self+.
289
+ # - #limit!: Replaces <tt>*</tt> with a given maximum value and removes all
290
+ # members over that maximum; returns +self+.
291
+ #
292
+ # === Methods for \IMAP String Formatting
293
+ #
294
+ # - #to_s: Returns the +sequence-set+ string, or an empty string when the
295
+ # set is empty.
296
+ # - #string: Returns the +sequence-set+ string, or nil when empty.
297
+ # - #valid_string: Returns the +sequence-set+ string, or raises
298
+ # DataFormatError when the set is empty.
299
+ # - #normalized_string: Returns a <tt>sequence-set</tt> string with its
300
+ # elements sorted and coalesced, or nil when the set is empty.
301
+ # - #normalize: Returns a new set with this set's normalized +sequence-set+
302
+ # representation.
303
+ # - #normalize!: Updates #string to its normalized +sequence-set+
304
+ # representation and returns +self+.
305
+ #
306
+ class SequenceSet
307
+ # The largest possible non-zero unsigned 32-bit integer
308
+ UINT32_MAX = 2**32 - 1
309
+
310
+ # represents "*" internally, to simplify sorting (etc)
311
+ STAR_INT = UINT32_MAX + 1
312
+ private_constant :STAR_INT
313
+
314
+ # valid inputs for "*"
315
+ STARS = [:*, ?*, -1].freeze
316
+ private_constant :STARS
317
+
318
+ class << self
319
+
320
+ # :call-seq:
321
+ # SequenceSet[*values] -> valid frozen sequence set
322
+ #
323
+ # Returns a frozen SequenceSet, constructed from +values+.
324
+ #
325
+ # An empty SequenceSet is invalid and will raise a DataFormatError.
326
+ #
327
+ # Use ::new to create a mutable or empty SequenceSet.
328
+ def [](first, *rest)
329
+ if rest.empty?
330
+ if first.is_a?(SequenceSet) && first.frozen? && first.valid?
331
+ first
332
+ else
333
+ new(first).validate.freeze
334
+ end
335
+ else
336
+ new(first).merge(*rest).validate.freeze
337
+ end
338
+ end
339
+
340
+ # :call-seq:
341
+ # SequenceSet.try_convert(obj) -> sequence set or nil
342
+ #
343
+ # If +obj+ is a SequenceSet, returns +obj+. If +obj+ responds_to
344
+ # +to_sequence_set+, calls +obj.to_sequence_set+ and returns the result.
345
+ # Otherwise returns +nil+.
346
+ #
347
+ # If +obj.to_sequence_set+ doesn't return a SequenceSet, an exception is
348
+ # raised.
349
+ def try_convert(obj)
350
+ return obj if obj.is_a?(SequenceSet)
351
+ return nil unless obj.respond_to?(:to_sequence_set)
352
+ obj = obj.to_sequence_set
353
+ return obj if obj.is_a?(SequenceSet)
354
+ raise DataFormatError, "invalid object returned from to_sequence_set"
355
+ end
356
+
357
+ # Returns a frozen empty set singleton. Note that valid \IMAP sequence
358
+ # sets cannot be empty, so this set is _invalid_.
359
+ def empty; EMPTY end
360
+
361
+ # Returns a frozen full set singleton: <tt>"1:*"</tt>
362
+ def full; FULL end
363
+
364
+ end
365
+
366
+ # Create a new SequenceSet object from +input+, which may be another
367
+ # SequenceSet, an IMAP formatted +sequence-set+ string, a number, a
368
+ # range, <tt>:*</tt>, or an enumerable of these.
369
+ #
370
+ # Use ::[] to create a frozen (non-empty) SequenceSet.
371
+ def initialize(input = nil) input ? replace(input) : clear end
372
+
373
+ # Removes all elements and returns self.
374
+ def clear; @tuples, @string = [], nil; self end
375
+
376
+ # Replace the contents of the set with the contents of +other+ and returns
377
+ # +self+.
378
+ #
379
+ # +other+ may be another SequenceSet, or it may be an IMAP +sequence-set+
380
+ # string, a number, a range, <tt>*</tt>, or an enumerable of these.
381
+ def replace(other)
382
+ case other
383
+ when SequenceSet then initialize_dup(other)
384
+ when String then self.string = other
385
+ else clear; merge other
386
+ end
387
+ self
388
+ end
389
+
390
+ # Returns the \IMAP +sequence-set+ string representation, or raises a
391
+ # DataFormatError when the set is empty.
392
+ #
393
+ # Use #string to return +nil+ or #to_s to return an empty string without
394
+ # error.
395
+ #
396
+ # Related: #string, #normalized_string, #to_s
397
+ def valid_string
398
+ raise DataFormatError, "empty sequence-set" if empty?
399
+ string
400
+ end
401
+
402
+ # Returns the \IMAP +sequence-set+ string representation, or +nil+ when
403
+ # the set is empty. Note that an empty set is invalid in the \IMAP
404
+ # syntax.
405
+ #
406
+ # Use #valid_string to raise an exception when the set is empty, or #to_s
407
+ # to return an empty string.
408
+ #
409
+ # If the set was created from a single string, it is not normalized. If
410
+ # the set is updated the string will be normalized.
411
+ #
412
+ # Related: #valid_string, #normalized_string, #to_s
413
+ def string; @string ||= normalized_string if valid? end
414
+
415
+ # Returns an array with #normalized_string when valid and an empty array
416
+ # otherwise.
417
+ def deconstruct; valid? ? [normalized_string] : [] end
418
+
419
+ # Assigns a new string to #string and resets #elements to match. It
420
+ # cannot be set to an empty string—assign +nil+ or use #clear instead.
421
+ # The string is validated but not normalized.
422
+ #
423
+ # Use #add or #merge to add a string to an existing set.
424
+ #
425
+ # Related: #replace, #clear
426
+ def string=(str)
427
+ if str.nil?
428
+ clear
429
+ else
430
+ str = String.try_convert(str) or raise ArgumentError, "not a string"
431
+ tuples = str_to_tuples str
432
+ @tuples, @string = [], -str
433
+ tuples_add tuples
434
+ end
435
+ end
436
+
437
+ # Returns the \IMAP +sequence-set+ string representation, or an empty
438
+ # string when the set is empty. Note that an empty set is invalid in the
439
+ # \IMAP syntax.
440
+ #
441
+ # Related: #valid_string, #normalized_string, #to_s
442
+ def to_s; string || "" end
443
+
444
+ # Freezes and returns the set. A frozen SequenceSet is Ractor-safe.
445
+ def freeze
446
+ return self if frozen?
447
+ string
448
+ @tuples.each(&:freeze).freeze
449
+ super
450
+ end
451
+
452
+ # :call-seq: self == other -> true or false
453
+ #
454
+ # Returns true when the other SequenceSet represents the same message
455
+ # identifiers. Encoding difference—such as order, overlaps, or
456
+ # duplicates—are ignored.
457
+ #
458
+ # Net::IMAP::SequenceSet["1:3"] == Net::IMAP::SequenceSet["1:3"]
459
+ # #=> true
460
+ # Net::IMAP::SequenceSet["1,2,3"] == Net::IMAP::SequenceSet["1:3"]
461
+ # #=> true
462
+ # Net::IMAP::SequenceSet["1,3"] == Net::IMAP::SequenceSet["3,1"]
463
+ # #=> true
464
+ # Net::IMAP::SequenceSet["9,1:*"] == Net::IMAP::SequenceSet["1:*"]
465
+ # #=> true
466
+ #
467
+ # Related: #eql?, #normalize
468
+ def ==(other)
469
+ self.class == other.class &&
470
+ (to_s == other.to_s || tuples == other.tuples)
471
+ end
472
+
473
+ # :call-seq: eql?(other) -> true or false
474
+ #
475
+ # Hash equality requires the same encoded #string representation.
476
+ #
477
+ # Net::IMAP::SequenceSet["1:3"] .eql? Net::IMAP::SequenceSet["1:3"]
478
+ # #=> true
479
+ # Net::IMAP::SequenceSet["1,2,3"].eql? Net::IMAP::SequenceSet["1:3"]
480
+ # #=> false
481
+ # Net::IMAP::SequenceSet["1,3"] .eql? Net::IMAP::SequenceSet["3,1"]
482
+ # #=> false
483
+ # Net::IMAP::SequenceSet["9,1:*"].eql? Net::IMAP::SequenceSet["1:*"]
484
+ # #=> false
485
+ #
486
+ # Related: #==, #normalize
487
+ def eql?(other) self.class == other.class && string == other.string end
488
+
489
+ # See #eql?
490
+ def hash; [self.class, string].hash end
491
+
492
+ # :call-seq: self === other -> true | false | nil
493
+ #
494
+ # Returns whether +other+ is contained within the set. Returns +nil+ if a
495
+ # StandardError is raised while converting +other+ to a comparable type.
496
+ #
497
+ # Related: #cover?, #include?, #include_star?
498
+ def ===(other)
499
+ cover?(other)
500
+ rescue
501
+ nil
502
+ end
503
+
504
+ # :call-seq: cover?(other) -> true | false | nil
505
+ #
506
+ # Returns whether +other+ is contained within the set. +other+ may be any
507
+ # object that would be accepted by ::new.
508
+ #
509
+ # Related: #===, #include?, #include_star?
510
+ def cover?(other) input_to_tuples(other).none? { !include_tuple?(_1) } end
511
+
512
+ # Returns +true+ when a given number or range is in +self+, and +false+
513
+ # otherwise. Returns +false+ unless +number+ is an Integer, Range, or
514
+ # <tt>*</tt>.
515
+ #
516
+ # set = Net::IMAP::SequenceSet["5:10,100,111:115"]
517
+ # set.include? 1 #=> false
518
+ # set.include? 5..10 #=> true
519
+ # set.include? 11..20 #=> false
520
+ # set.include? 100 #=> true
521
+ # set.include? 6 #=> true, covered by "5:10"
522
+ # set.include? 4..9 #=> true, covered by "5:10"
523
+ # set.include? "4:9" #=> true, strings are parsed
524
+ # set.include? 4..9 #=> false, intersection is not sufficient
525
+ # set.include? "*" #=> false, use #limit to re-interpret "*"
526
+ # set.include? -1 #=> false, -1 is interpreted as "*"
527
+ #
528
+ # set = Net::IMAP::SequenceSet["5:10,100,111:*"]
529
+ # set.include? :* #=> true
530
+ # set.include? "*" #=> true
531
+ # set.include? -1 #=> true
532
+ # set.include? 200.. #=> true
533
+ # set.include? 100.. #=> false
534
+ #
535
+ # Related: #include_star?, #cover?, #===
536
+ def include?(element) include_tuple? input_to_tuple element end
537
+
538
+ alias member? include?
539
+
540
+ # Returns +true+ when the set contains <tt>*</tt>.
541
+ def include_star?; @tuples.last&.last == STAR_INT end
542
+
543
+ # Returns +true+ if the set and a given object have any common elements,
544
+ # +false+ otherwise.
545
+ #
546
+ # Net::IMAP::SequenceSet["5:10"].intersect? "7,9,11" #=> true
547
+ # Net::IMAP::SequenceSet["5:10"].intersect? "11:33" #=> false
548
+ #
549
+ # Related: #intersection, #disjoint?
550
+ def intersect?(other)
551
+ valid? && input_to_tuples(other).any? { intersect_tuple? _1 }
552
+ end
553
+ alias overlap? intersect?
554
+
555
+ # Returns +true+ if the set and a given object have no common elements,
556
+ # +false+ otherwise.
557
+ #
558
+ # Net::IMAP::SequenceSet["5:10"].disjoint? "7,9,11" #=> false
559
+ # Net::IMAP::SequenceSet["5:10"].disjoint? "11:33" #=> true
560
+ #
561
+ # Related: #intersection, #intersect?
562
+ def disjoint?(other)
563
+ empty? || input_to_tuples(other).none? { intersect_tuple? _1 }
564
+ end
565
+
566
+ # :call-seq: max(star: :*) => integer or star or nil
567
+ #
568
+ # Returns the maximum value in +self+, +star+ when the set includes
569
+ # <tt>*</tt>, or +nil+ when the set is empty.
570
+ def max(star: :*)
571
+ (val = @tuples.last&.last) && val == STAR_INT ? star : val
572
+ end
573
+
574
+ # :call-seq: min(star: :*) => integer or star or nil
575
+ #
576
+ # Returns the minimum value in +self+, +star+ when the only value in the
577
+ # set is <tt>*</tt>, or +nil+ when the set is empty.
578
+ def min(star: :*)
579
+ (val = @tuples.first&.first) && val == STAR_INT ? star : val
580
+ end
581
+
582
+ # :call-seq: minmax(star: :*) => nil or [integer, integer or star]
583
+ #
584
+ # Returns a 2-element array containing the minimum and maximum numbers in
585
+ # +self+, or +nil+ when the set is empty.
586
+ def minmax(star: :*); [min(star: star), max(star: star)] unless empty? end
587
+
588
+ # Returns false when the set is empty.
589
+ def valid?; !empty? end
590
+
591
+ # Returns true if the set contains no elements
592
+ def empty?; @tuples.empty? end
593
+
594
+ # Returns true if the set contains every possible element.
595
+ def full?; @tuples == [[1, STAR_INT]] end
596
+
597
+ # :call-seq:
598
+ # self + other -> sequence set
599
+ # self | other -> sequence set
600
+ # union(other) -> sequence set
601
+ #
602
+ # Returns a new sequence set that has every number in the +other+ object
603
+ # added.
604
+ #
605
+ # +other+ may be any object that would be accepted by ::new: a non-zero 32
606
+ # bit unsigned integer, range, <tt>sequence-set</tt> formatted string,
607
+ # another sequence set, or an enumerable containing any of these.
608
+ #
609
+ # Net::IMAP::SequenceSet["1:5"] | 2 | [4..6, 99]
610
+ # #=> Net::IMAP::SequenceSet["1:6,99"]
611
+ #
612
+ # Related: #add, #merge
613
+ def |(other) remain_frozen dup.merge other end
614
+ alias :+ :|
615
+ alias union :|
616
+
617
+ # :call-seq:
618
+ # self - other -> sequence set
619
+ # difference(other) -> sequence set
620
+ #
621
+ # Returns a new sequence set built by duplicating this set and removing
622
+ # every number that appears in +other+.
623
+ #
624
+ # +other+ may be any object that would be accepted by ::new: a non-zero 32
625
+ # bit unsigned integer, range, <tt>sequence-set</tt> formatted string,
626
+ # another sequence set, or an enumerable containing any of these.
627
+ #
628
+ # Net::IMAP::SequenceSet[1..5] - 2 - 4 - 6
629
+ # #=> Net::IMAP::SequenceSet["1,3,5"]
630
+ #
631
+ # Related: #subtract
632
+ def -(other) remain_frozen dup.subtract other end
633
+ alias difference :-
634
+
635
+ # :call-seq:
636
+ # self & other -> sequence set
637
+ # intersection(other) -> sequence set
638
+ #
639
+ # Returns a new sequence set containing only the numbers common to this
640
+ # set and +other+.
641
+ #
642
+ # +other+ may be any object that would be accepted by ::new: a non-zero 32
643
+ # bit unsigned integer, range, <tt>sequence-set</tt> formatted string,
644
+ # another sequence set, or an enumerable containing any of these.
645
+ #
646
+ # Net::IMAP::SequenceSet[1..5] & [2, 4, 6]
647
+ # #=> Net::IMAP::SequenceSet["2,4"]
648
+ #
649
+ # <tt>(seqset & other)</tt> is equivalent to <tt>(seqset - ~other)</tt>.
650
+ def &(other)
651
+ remain_frozen dup.subtract SequenceSet.new(other).complement!
652
+ end
653
+ alias intersection :&
654
+
655
+ # :call-seq:
656
+ # self ^ other -> sequence set
657
+ # xor(other) -> sequence set
658
+ #
659
+ # Returns a new sequence set containing numbers that are exclusive between
660
+ # this set and +other+.
661
+ #
662
+ # +other+ may be any object that would be accepted by ::new: a non-zero 32
663
+ # bit unsigned integer, range, <tt>sequence-set</tt> formatted string,
664
+ # another sequence set, or an enumerable containing any of these.
665
+ #
666
+ # Net::IMAP::SequenceSet[1..5] ^ [2, 4, 6]
667
+ # #=> Net::IMAP::SequenceSet["1,3,5:6"]
668
+ #
669
+ # <tt>(seqset ^ other)</tt> is equivalent to <tt>((seqset | other) -
670
+ # (seqset & other))</tt>.
671
+ def ^(other) remain_frozen (self | other).subtract(self & other) end
672
+ alias xor :^
673
+
674
+ # :call-seq:
675
+ # ~ self -> sequence set
676
+ # complement -> sequence set
677
+ #
678
+ # Returns the complement of self, a SequenceSet which contains all numbers
679
+ # _except_ for those in this set.
680
+ #
681
+ # ~Net::IMAP::SequenceSet.full #=> Net::IMAP::SequenceSet.empty
682
+ # ~Net::IMAP::SequenceSet.empty #=> Net::IMAP::SequenceSet.full
683
+ # ~Net::IMAP::SequenceSet["1:5,100:222"]
684
+ # #=> Net::IMAP::SequenceSet["6:99,223:*"]
685
+ # ~Net::IMAP::SequenceSet["6:99,223:*"]
686
+ # #=> Net::IMAP::SequenceSet["1:5,100:222"]
687
+ #
688
+ # Related: #complement!
689
+ def ~; remain_frozen dup.complement! end
690
+ alias complement :~
691
+
692
+ # :call-seq:
693
+ # add(object) -> self
694
+ # self << other -> self
695
+ #
696
+ # Adds a range or number to the set and returns +self+.
697
+ #
698
+ # #string will be regenerated. Use #merge to add many elements at once.
699
+ #
700
+ # Related: #add?, #merge, #union
701
+ def add(object)
702
+ tuple_add input_to_tuple object
703
+ normalize!
704
+ end
705
+ alias << add
706
+
707
+ # Adds a range or number to the set and returns +self+.
708
+ #
709
+ # Unlike #add, #merge, or #union, the new value is appended to #string.
710
+ # This may result in a #string which has duplicates or is out-of-order.
711
+ def append(object)
712
+ modifying!
713
+ tuple = input_to_tuple object
714
+ entry = tuple_to_str tuple
715
+ string unless empty? # write @string before tuple_add
716
+ tuple_add tuple
717
+ @string = -(@string ? "#{@string},#{entry}" : entry)
718
+ self
719
+ end
720
+
721
+ # :call-seq: add?(object) -> self or nil
722
+ #
723
+ # Adds a range or number to the set and returns +self+. Returns +nil+
724
+ # when the object is already included in the set.
725
+ #
726
+ # #string will be regenerated. Use #merge to add many elements at once.
727
+ #
728
+ # Related: #add, #merge, #union, #include?
729
+ def add?(object)
730
+ add object unless include? object
731
+ end
732
+
733
+ # :call-seq: delete(object) -> self
734
+ #
735
+ # Deletes the given range or number from the set and returns +self+.
736
+ #
737
+ # #string will be regenerated after deletion. Use #subtract to remove
738
+ # many elements at once.
739
+ #
740
+ # Related: #delete?, #delete_at, #subtract, #difference
741
+ def delete(object)
742
+ tuple_subtract input_to_tuple object
743
+ normalize!
744
+ end
745
+
746
+ # :call-seq:
747
+ # delete?(number) -> integer or nil
748
+ # delete?(star) -> :* or nil
749
+ # delete?(range) -> sequence set or nil
750
+ #
751
+ # Removes a specified value from the set, and returns the removed value.
752
+ # Returns +nil+ if nothing was removed.
753
+ #
754
+ # Returns an integer when the specified +number+ argument was removed:
755
+ # set = Net::IMAP::SequenceSet.new [5..10, 20]
756
+ # set.delete?(7) #=> 7
757
+ # set #=> #<Net::IMAP::SequenceSet "5:6,8:10,20">
758
+ # set.delete?("20") #=> 20
759
+ # set #=> #<Net::IMAP::SequenceSet "5:6,8:10">
760
+ # set.delete?(30) #=> nil
761
+ #
762
+ # Returns <tt>:*</tt> when <tt>*</tt> or <tt>-1</tt> is specified and
763
+ # removed:
764
+ # set = Net::IMAP::SequenceSet.new "5:9,20,35,*"
765
+ # set.delete?(-1) #=> :*
766
+ # set #=> #<Net::IMAP::SequenceSet "5:9,20,35">
767
+ #
768
+ # And returns a new SequenceSet when a range is specified:
769
+ #
770
+ # set = Net::IMAP::SequenceSet.new [5..10, 20]
771
+ # set.delete?(9..) #=> #<Net::IMAP::SequenceSet "9:10,20">
772
+ # set #=> #<Net::IMAP::SequenceSet "5:8">
773
+ # set.delete?(21..) #=> nil
774
+ #
775
+ # #string will be regenerated after deletion.
776
+ #
777
+ # Related: #delete, #delete_at, #subtract, #difference, #disjoint?
778
+ def delete?(object)
779
+ tuple = input_to_tuple object
780
+ if tuple.first == tuple.last
781
+ return unless include_tuple? tuple
782
+ tuple_subtract tuple
783
+ normalize!
784
+ from_tuple_int tuple.first
785
+ else
786
+ copy = dup
787
+ tuple_subtract tuple
788
+ normalize!
789
+ copy if copy.subtract(self).valid?
790
+ end
791
+ end
792
+
793
+ # :call-seq: delete_at(index) -> number or :* or nil
794
+ #
795
+ # Deletes a number the set, indicated by the given +index+. Returns the
796
+ # number that was removed, or +nil+ if nothing was removed.
797
+ #
798
+ # #string will be regenerated after deletion.
799
+ #
800
+ # Related: #delete, #delete?, #slice!, #subtract, #difference
801
+ def delete_at(index)
802
+ slice! Integer(index.to_int)
803
+ end
804
+
805
+ # :call-seq:
806
+ # slice!(index) -> integer or :* or nil
807
+ # slice!(start, length) -> sequence set or nil
808
+ # slice!(range) -> sequence set or nil
809
+ #
810
+ # Deletes a number or consecutive numbers from the set, indicated by the
811
+ # given +index+, +start+ and +length+, or +range+ of offsets. Returns the
812
+ # number or sequence set that was removed, or +nil+ if nothing was
813
+ # removed. Arguments are interpreted the same as for #slice or #[].
814
+ #
815
+ # #string will be regenerated after deletion.
816
+ #
817
+ # Related: #slice, #delete_at, #delete, #delete?, #subtract, #difference
818
+ def slice!(index, length = nil)
819
+ deleted = slice(index, length) and subtract deleted
820
+ deleted
821
+ end
822
+
823
+ # Merges all of the elements that appear in any of the +inputs+ into the
824
+ # set, and returns +self+.
825
+ #
826
+ # The +inputs+ may be any objects that would be accepted by ::new:
827
+ # non-zero 32 bit unsigned integers, ranges, <tt>sequence-set</tt>
828
+ # formatted strings, other sequence sets, or enumerables containing any of
829
+ # these.
830
+ #
831
+ # #string will be regenerated after all inputs have been merged.
832
+ #
833
+ # Related: #add, #add?, #union
834
+ def merge(*inputs)
835
+ tuples_add input_to_tuples inputs
836
+ normalize!
837
+ end
838
+
839
+ # Removes all of the elements that appear in any of the given +objects+
840
+ # from the set, and returns +self+.
841
+ #
842
+ # The +objects+ may be any objects that would be accepted by ::new:
843
+ # non-zero 32 bit unsigned integers, ranges, <tt>sequence-set</tt>
844
+ # formatted strings, other sequence sets, or enumerables containing any of
845
+ # these.
846
+ #
847
+ # Related: #difference
848
+ def subtract(*objects)
849
+ tuples_subtract input_to_tuples objects
850
+ normalize!
851
+ end
852
+
853
+ # Returns an array of ranges and integers and <tt>:*</tt>.
854
+ #
855
+ # The entries are in the same order they appear in #string, with no
856
+ # sorting, deduplication, or coalescing. When #string is in its
857
+ # normalized form, this will return the same result as #elements.
858
+ # This is useful when the given order is significant, for example in a
859
+ # ESEARCH response to IMAP#sort.
860
+ #
861
+ # Related: #each_entry, #elements
862
+ def entries; each_entry.to_a end
863
+
864
+ # Returns an array of ranges and integers and <tt>:*</tt>.
865
+ #
866
+ # The returned elements are sorted and coalesced, even when the input
867
+ # #string is not. <tt>*</tt> will sort last. See #normalize.
868
+ #
869
+ # By itself, <tt>*</tt> translates to <tt>:*</tt>. A range containing
870
+ # <tt>*</tt> translates to an endless range. Use #limit to translate both
871
+ # cases to a maximum value.
872
+ #
873
+ # The returned elements will be sorted and coalesced, even when the input
874
+ # #string is not. <tt>*</tt> will sort last. See #normalize.
875
+ #
876
+ # Net::IMAP::SequenceSet["2,5:9,6,*,12:11"].elements
877
+ # #=> [2, 5..9, 11..12, :*]
878
+ #
879
+ # Related: #each_element, #ranges, #numbers
880
+ def elements; each_element.to_a end
881
+ alias to_a elements
882
+
883
+ # Returns an array of ranges
884
+ #
885
+ # The returned elements are sorted and coalesced, even when the input
886
+ # #string is not. <tt>*</tt> will sort last. See #normalize.
887
+ #
888
+ # <tt>*</tt> translates to an endless range. By itself, <tt>*</tt>
889
+ # translates to <tt>:*..</tt>. Use #limit to set <tt>*</tt> to a maximum
890
+ # value.
891
+ #
892
+ # The returned ranges will be sorted and coalesced, even when the input
893
+ # #string is not. <tt>*</tt> will sort last. See #normalize.
894
+ #
895
+ # Net::IMAP::SequenceSet["2,5:9,6,*,12:11"].ranges
896
+ # #=> [2..2, 5..9, 11..12, :*..]
897
+ # Net::IMAP::SequenceSet["123,999:*,456:789"].ranges
898
+ # #=> [123..123, 456..789, 999..]
899
+ #
900
+ # Related: #each_range, #elements, #numbers, #to_set
901
+ def ranges; each_range.to_a end
902
+
903
+ # Returns a sorted array of all of the number values in the sequence set.
904
+ #
905
+ # The returned numbers are sorted and de-duplicated, even when the input
906
+ # #string is not. See #normalize.
907
+ #
908
+ # Net::IMAP::SequenceSet["2,5:9,6,12:11"].numbers
909
+ # #=> [2, 5, 6, 7, 8, 9, 11, 12]
910
+ #
911
+ # If the set contains a <tt>*</tt>, RangeError is raised. See #limit.
912
+ #
913
+ # Net::IMAP::SequenceSet["10000:*"].numbers
914
+ # #!> RangeError
915
+ #
916
+ # *WARNING:* Even excluding sets with <tt>*</tt>, an enormous result can
917
+ # easily be created. An array with over 4 billion integers could be
918
+ # returned, requiring up to 32GiB of memory on a 64-bit architecture.
919
+ #
920
+ # Net::IMAP::SequenceSet[10000..2**32-1].numbers
921
+ # # ...probably freezes the process for a while...
922
+ # #!> NoMemoryError (probably)
923
+ #
924
+ # For safety, consider using #limit or #intersection to set an upper
925
+ # bound. Alternatively, use #each_element, #each_range, or even
926
+ # #each_number to avoid allocation of a result array.
927
+ #
928
+ # Related: #elements, #ranges, #to_set
929
+ def numbers; each_number.to_a end
930
+
931
+ # Yields each number or range in #string to the block and returns +self+.
932
+ # Returns an enumerator when called without a block.
933
+ #
934
+ # The entries are yielded in the same order they appear in #string, with
935
+ # no sorting, deduplication, or coalescing. When #string is in its
936
+ # normalized form, this will yield the same values as #each_element.
937
+ #
938
+ # Related: #entries, #each_element
939
+ def each_entry(&block) # :yields: integer or range or :*
940
+ return to_enum(__method__) unless block_given?
941
+ each_entry_tuple do yield tuple_to_entry _1 end
942
+ end
943
+
944
+ # Yields each number or range (or <tt>:*</tt>) in #elements to the block
945
+ # and returns self. Returns an enumerator when called without a block.
946
+ #
947
+ # The returned numbers are sorted and de-duplicated, even when the input
948
+ # #string is not. See #normalize.
949
+ #
950
+ # Related: #elements, #each_entry
951
+ def each_element # :yields: integer or range or :*
952
+ return to_enum(__method__) unless block_given?
953
+ @tuples.each do yield tuple_to_entry _1 end
954
+ self
955
+ end
956
+
957
+ private
958
+
959
+ def each_entry_tuple(&block)
960
+ return to_enum(__method__) unless block_given?
961
+ if @string
962
+ @string.split(",") do block.call str_to_tuple _1 end
963
+ else
964
+ @tuples.each(&block)
965
+ end
966
+ self
967
+ end
968
+
969
+ def tuple_to_entry((min, max))
970
+ if min == STAR_INT then :*
971
+ elsif max == STAR_INT then min..
972
+ elsif min == max then min
973
+ else min..max
974
+ end
975
+ end
976
+
977
+ public
978
+
979
+ # Yields each range in #ranges to the block and returns self.
980
+ # Returns an enumerator when called without a block.
981
+ #
982
+ # Related: #ranges
983
+ def each_range # :yields: range
984
+ return to_enum(__method__) unless block_given?
985
+ @tuples.each do |min, max|
986
+ if min == STAR_INT then yield :*..
987
+ elsif max == STAR_INT then yield min..
988
+ else yield min..max
989
+ end
990
+ end
991
+ self
992
+ end
993
+
994
+ # Yields each number in #numbers to the block and returns self.
995
+ # If the set contains a <tt>*</tt>, RangeError will be raised.
996
+ #
997
+ # Returns an enumerator when called without a block (even if the set
998
+ # contains <tt>*</tt>).
999
+ #
1000
+ # Related: #numbers, #each_ordered_number
1001
+ def each_number(&block) # :yields: integer
1002
+ return to_enum(__method__) unless block_given?
1003
+ raise RangeError, '%s contains "*"' % [self.class] if include_star?
1004
+ @tuples.each do each_number_in_tuple _1, _2, &block end
1005
+ self
1006
+ end
1007
+
1008
+ # Yields each number in #entries to the block and returns self.
1009
+ # If the set contains a <tt>*</tt>, RangeError will be raised.
1010
+ #
1011
+ # Returns an enumerator when called without a block (even if the set
1012
+ # contains <tt>*</tt>).
1013
+ #
1014
+ # Related: #entries, #each_number
1015
+ def each_ordered_number(&block)
1016
+ return to_enum(__method__) unless block_given?
1017
+ raise RangeError, '%s contains "*"' % [self.class] if include_star?
1018
+ each_entry_tuple do each_number_in_tuple _1, _2, &block end
1019
+ end
1020
+
1021
+ private def each_number_in_tuple(min, max, &block)
1022
+ if min == STAR_INT then yield :*
1023
+ elsif min == max then yield min
1024
+ elsif max != STAR_INT then (min..max).each(&block)
1025
+ else
1026
+ raise RangeError, "#{SequenceSet} cannot enumerate range with '*'"
1027
+ end
1028
+ end
1029
+
1030
+ # Returns a Set with all of the #numbers in the sequence set.
1031
+ #
1032
+ # If the set contains a <tt>*</tt>, RangeError will be raised.
1033
+ #
1034
+ # See #numbers for the warning about very large sets.
1035
+ #
1036
+ # Related: #elements, #ranges, #numbers
1037
+ def to_set; Set.new(numbers) end
1038
+
1039
+ # Returns the count of #numbers in the set.
1040
+ #
1041
+ # <tt>*</tt> will be counted as <tt>2**32 - 1</tt> (the maximum 32-bit
1042
+ # unsigned integer value).
1043
+ #
1044
+ # Related: #count_with_duplicates
1045
+ def count
1046
+ @tuples.sum(@tuples.count) { _2 - _1 } +
1047
+ (include_star? && include?(UINT32_MAX) ? -1 : 0)
1048
+ end
1049
+
1050
+ alias size count
1051
+
1052
+ # Returns the count of numbers in the ordered #entries, including any
1053
+ # repeated numbers.
1054
+ #
1055
+ # <tt>*</tt> will be counted as <tt>2**32 - 1</tt> (the maximum 32-bit
1056
+ # unsigned integer value).
1057
+ #
1058
+ # When #string is normalized, this behaves the same as #count.
1059
+ #
1060
+ # Related: #entries, #count_duplicates, #has_duplicates?
1061
+ def count_with_duplicates
1062
+ return count unless @string
1063
+ each_entry_tuple.sum {|min, max|
1064
+ max - min + ((max == STAR_INT && min != STAR_INT) ? 0 : 1)
1065
+ }
1066
+ end
1067
+
1068
+ # Returns the count of repeated numbers in the ordered #entries, the
1069
+ # difference between #count_with_duplicates and #count.
1070
+ #
1071
+ # When #string is normalized, this is zero.
1072
+ #
1073
+ # Related: #entries, #count_with_duplicates, #has_duplicates?
1074
+ def count_duplicates
1075
+ return 0 unless @string
1076
+ count_with_duplicates - count
1077
+ end
1078
+
1079
+ # :call-seq: has_duplicates? -> true | false
1080
+ #
1081
+ # Returns whether or not the ordered #entries repeat any numbers.
1082
+ #
1083
+ # Always returns +false+ when #string is normalized.
1084
+ #
1085
+ # Related: #entries, #count_with_duplicates, #count_duplicates?
1086
+ def has_duplicates?
1087
+ return false unless @string
1088
+ count_with_duplicates != count
1089
+ end
1090
+
1091
+ # Returns the (sorted and deduplicated) index of +number+ in the set, or
1092
+ # +nil+ if +number+ isn't in the set.
1093
+ #
1094
+ # Related: #[], #at, #find_ordered_index
1095
+ def find_index(number)
1096
+ number = to_tuple_int number
1097
+ each_tuple_with_index(@tuples) do |min, max, idx_min|
1098
+ number < min and return nil
1099
+ number <= max and return from_tuple_int(idx_min + (number - min))
1100
+ end
1101
+ nil
1102
+ end
1103
+
1104
+ # Returns the first index of +number+ in the ordered #entries, or
1105
+ # +nil+ if +number+ isn't in the set.
1106
+ #
1107
+ # Related: #find_index
1108
+ def find_ordered_index(number)
1109
+ number = to_tuple_int number
1110
+ each_tuple_with_index(each_entry_tuple) do |min, max, idx_min|
1111
+ if min <= number && number <= max
1112
+ return from_tuple_int(idx_min + (number - min))
1113
+ end
1114
+ end
1115
+ nil
1116
+ end
1117
+
1118
+ private
1119
+
1120
+ def each_tuple_with_index(tuples)
1121
+ idx_min = 0
1122
+ tuples.each do |min, max|
1123
+ idx_max = idx_min + (max - min)
1124
+ yield min, max, idx_min, idx_max
1125
+ idx_min = idx_max + 1
1126
+ end
1127
+ idx_min
1128
+ end
1129
+
1130
+ def reverse_each_tuple_with_index(tuples)
1131
+ idx_max = -1
1132
+ tuples.reverse_each do |min, max|
1133
+ yield min, max, (idx_min = idx_max - (max - min)), idx_max
1134
+ idx_max = idx_min - 1
1135
+ end
1136
+ idx_max
1137
+ end
1138
+
1139
+ public
1140
+
1141
+ # :call-seq: at(index) -> integer or nil
1142
+ #
1143
+ # Returns the number at the given +index+ in the sorted set, without
1144
+ # modifying the set.
1145
+ #
1146
+ # +index+ is interpreted the same as in #[], except that #at only allows a
1147
+ # single integer argument.
1148
+ #
1149
+ # Related: #[], #slice, #ordered_at
1150
+ def at(index)
1151
+ lookup_number_by_tuple_index(tuples, index)
1152
+ end
1153
+
1154
+ # :call-seq: ordered_at(index) -> integer or nil
1155
+ #
1156
+ # Returns the number at the given +index+ in the ordered #entries, without
1157
+ # modifying the set.
1158
+ #
1159
+ # +index+ is interpreted the same as in #at (and #[]), except that
1160
+ # #ordered_at applies to the ordered #entries, not the sorted set.
1161
+ #
1162
+ # Related: #[], #slice, #ordered_at
1163
+ def ordered_at(index)
1164
+ lookup_number_by_tuple_index(each_entry_tuple, index)
1165
+ end
1166
+
1167
+ private def lookup_number_by_tuple_index(tuples, index)
1168
+ index = Integer(index.to_int)
1169
+ if index.negative?
1170
+ reverse_each_tuple_with_index(tuples) do |min, max, idx_min, idx_max|
1171
+ idx_min <= index and return from_tuple_int(min + (index - idx_min))
1172
+ end
1173
+ else
1174
+ each_tuple_with_index(tuples) do |min, _, idx_min, idx_max|
1175
+ index <= idx_max and return from_tuple_int(min + (index - idx_min))
1176
+ end
1177
+ end
1178
+ nil
1179
+ end
1180
+
1181
+ # :call-seq:
1182
+ # seqset[index] -> integer or :* or nil
1183
+ # slice(index) -> integer or :* or nil
1184
+ # seqset[start, length] -> sequence set or nil
1185
+ # slice(start, length) -> sequence set or nil
1186
+ # seqset[range] -> sequence set or nil
1187
+ # slice(range) -> sequence set or nil
1188
+ #
1189
+ # Returns a number or a subset from the _sorted_ set, without modifying
1190
+ # the set.
1191
+ #
1192
+ # When an Integer argument +index+ is given, the number at offset +index+
1193
+ # in the sorted set is returned:
1194
+ #
1195
+ # set = Net::IMAP::SequenceSet["10:15,20:23,26"]
1196
+ # set[0] #=> 10
1197
+ # set[5] #=> 15
1198
+ # set[10] #=> 26
1199
+ #
1200
+ # If +index+ is negative, it counts relative to the end of the sorted set:
1201
+ # set = Net::IMAP::SequenceSet["10:15,20:23,26"]
1202
+ # set[-1] #=> 26
1203
+ # set[-3] #=> 22
1204
+ # set[-6] #=> 15
1205
+ #
1206
+ # If +index+ is out of range, +nil+ is returned.
1207
+ #
1208
+ # set = Net::IMAP::SequenceSet["10:15,20:23,26"]
1209
+ # set[11] #=> nil
1210
+ # set[-12] #=> nil
1211
+ #
1212
+ # The result is based on the sorted and de-duplicated set, not on the
1213
+ # ordered #entries in #string.
1214
+ #
1215
+ # set = Net::IMAP::SequenceSet["12,20:23,11:16,21"]
1216
+ # set[0] #=> 11
1217
+ # set[-1] #=> 23
1218
+ #
1219
+ # Related: #at
1220
+ def [](index, length = nil)
1221
+ if length then slice_length(index, length)
1222
+ elsif index.is_a?(Range) then slice_range(index)
1223
+ else at(index)
1224
+ end
1225
+ end
1226
+
1227
+ alias slice :[]
1228
+
1229
+ private
1230
+
1231
+ def slice_length(start, length)
1232
+ start = Integer(start.to_int)
1233
+ length = Integer(length.to_int)
1234
+ raise ArgumentError, "length must be positive" unless length.positive?
1235
+ last = start + length - 1 unless start.negative? && start.abs <= length
1236
+ slice_range(start..last)
1237
+ end
1238
+
1239
+ def slice_range(range)
1240
+ first = range.begin || 0
1241
+ last = range.end || -1
1242
+ last -= 1 if range.exclude_end? && range.end && last != STAR_INT
1243
+ if (first * last).positive? && last < first
1244
+ SequenceSet.empty
1245
+ elsif (min = at(first))
1246
+ max = at(last)
1247
+ if max == :* then self & (min..)
1248
+ elsif min <= max then self & (min..max)
1249
+ else SequenceSet.empty
1250
+ end
1251
+ end
1252
+ end
1253
+
1254
+ public
1255
+
1256
+ # Returns a frozen SequenceSet with <tt>*</tt> converted to +max+, numbers
1257
+ # and ranges over +max+ removed, and ranges containing +max+ converted to
1258
+ # end at +max+.
1259
+ #
1260
+ # Net::IMAP::SequenceSet["5,10:22,50"].limit(max: 20).to_s
1261
+ # #=> "5,10:20"
1262
+ #
1263
+ # <tt>*</tt> is always interpreted as the maximum value. When the set
1264
+ # contains <tt>*</tt>, it will be set equal to the limit.
1265
+ #
1266
+ # Net::IMAP::SequenceSet["*"].limit(max: 37)
1267
+ # #=> Net::IMAP::SequenceSet["37"]
1268
+ # Net::IMAP::SequenceSet["5:*"].limit(max: 37)
1269
+ # #=> Net::IMAP::SequenceSet["5:37"]
1270
+ # Net::IMAP::SequenceSet["500:*"].limit(max: 37)
1271
+ # #=> Net::IMAP::SequenceSet["37"]
1272
+ #
1273
+ def limit(max:)
1274
+ max = to_tuple_int(max)
1275
+ if empty? then self.class.empty
1276
+ elsif !include_star? && max < min then self.class.empty
1277
+ elsif max(star: STAR_INT) <= max then frozen? ? self : dup.freeze
1278
+ else dup.limit!(max: max).freeze
1279
+ end
1280
+ end
1281
+
1282
+ # Removes all members over +max+ and returns self. If <tt>*</tt> is a
1283
+ # member, it will be converted to +max+.
1284
+ #
1285
+ # Related: #limit
1286
+ def limit!(max:)
1287
+ star = include_star?
1288
+ max = to_tuple_int(max)
1289
+ tuple_subtract [max + 1, STAR_INT]
1290
+ tuple_add [max, max ] if star
1291
+ normalize!
1292
+ end
1293
+
1294
+ # :call-seq: complement! -> self
1295
+ #
1296
+ # Converts the SequenceSet to its own #complement. It will contain all
1297
+ # possible values _except_ for those currently in the set.
1298
+ #
1299
+ # Related: #complement
1300
+ def complement!
1301
+ return replace(self.class.full) if empty?
1302
+ return clear if full?
1303
+ flat = @tuples.flat_map { [_1 - 1, _2 + 1] }
1304
+ if flat.first < 1 then flat.shift else flat.unshift 1 end
1305
+ if STAR_INT < flat.last then flat.pop else flat.push STAR_INT end
1306
+ @tuples = flat.each_slice(2).to_a
1307
+ normalize!
1308
+ end
1309
+
1310
+ # Returns a new SequenceSet with a normalized string representation.
1311
+ #
1312
+ # The returned set's #string is sorted and deduplicated. Adjacent or
1313
+ # overlapping elements will be merged into a single larger range.
1314
+ #
1315
+ # Net::IMAP::SequenceSet["1:5,3:7,10:9,10:11"].normalize
1316
+ # #=> Net::IMAP::SequenceSet["1:7,9:11"]
1317
+ #
1318
+ # Related: #normalize!, #normalized_string
1319
+ def normalize
1320
+ str = normalized_string
1321
+ return self if frozen? && str == string
1322
+ remain_frozen dup.instance_exec { @string = str&.-@; self }
1323
+ end
1324
+
1325
+ # Resets #string to be sorted, deduplicated, and coalesced. Returns
1326
+ # +self+.
1327
+ #
1328
+ # Related: #normalize, #normalized_string
1329
+ def normalize!
1330
+ @string = nil
1331
+ self
1332
+ end
1333
+
1334
+ # Returns a normalized +sequence-set+ string representation, sorted
1335
+ # and deduplicated. Adjacent or overlapping elements will be merged into
1336
+ # a single larger range. Returns +nil+ when the set is empty.
1337
+ #
1338
+ # Net::IMAP::SequenceSet["1:5,3:7,10:9,10:11"].normalized_string
1339
+ # #=> "1:7,9:11"
1340
+ #
1341
+ # Related: #normalize!, #normalize
1342
+ def normalized_string
1343
+ @tuples.empty? ? nil : -@tuples.map { tuple_to_str _1 }.join(",")
1344
+ end
1345
+
1346
+ def inspect
1347
+ if empty?
1348
+ (frozen? ? "%s.empty" : "#<%s empty>") % [self.class]
1349
+ elsif frozen?
1350
+ "%s[%p]" % [self.class, to_s]
1351
+ else
1352
+ "#<%s %p>" % [self.class, to_s]
1353
+ end
1354
+ end
1355
+
1356
+ # Returns self
1357
+ alias to_sequence_set itself
1358
+
1359
+ # Unstable API: currently for internal use only (Net::IMAP#validate_data)
1360
+ def validate # :nodoc:
1361
+ empty? and raise DataFormatError, "empty sequence-set is invalid"
1362
+ self
1363
+ end
1364
+
1365
+ # Unstable API: for internal use only (Net::IMAP#send_data)
1366
+ def send_data(imap, tag) # :nodoc:
1367
+ imap.__send__(:put_string, valid_string)
1368
+ end
1369
+
1370
+ protected
1371
+
1372
+ attr_reader :tuples # :nodoc:
1373
+
1374
+ private
1375
+
1376
+ def remain_frozen(set) frozen? ? set.freeze : set end
1377
+
1378
+ # frozen clones are shallow copied
1379
+ def initialize_clone(other)
1380
+ other.frozen? ? super : initialize_dup(other)
1381
+ end
1382
+
1383
+ def initialize_dup(other)
1384
+ @tuples = other.tuples.map(&:dup)
1385
+ @string = other.string&.-@
1386
+ super
1387
+ end
1388
+
1389
+ def input_to_tuple(obj)
1390
+ obj = input_try_convert obj
1391
+ case obj
1392
+ when *STARS, Integer then [int = to_tuple_int(obj), int]
1393
+ when Range then range_to_tuple(obj)
1394
+ when String then str_to_tuple(obj)
1395
+ else
1396
+ raise DataFormatError, "expected number or range, got %p" % [obj]
1397
+ end
1398
+ end
1399
+
1400
+ def input_to_tuples(obj)
1401
+ obj = input_try_convert obj
1402
+ case obj
1403
+ when *STARS, Integer, Range then [input_to_tuple(obj)]
1404
+ when String then str_to_tuples obj
1405
+ when SequenceSet then obj.tuples
1406
+ when Set then obj.map { [to_tuple_int(_1)] * 2 }
1407
+ when Array then obj.flat_map { input_to_tuples _1 }
1408
+ when nil then []
1409
+ else
1410
+ raise DataFormatError,
1411
+ "expected nz-number, range, string, or enumerable; " \
1412
+ "got %p" % [obj]
1413
+ end
1414
+ end
1415
+
1416
+ # unlike SequenceSet#try_convert, this returns an Integer, Range,
1417
+ # String, Set, Array, or... any type of object.
1418
+ def input_try_convert(input)
1419
+ SequenceSet.try_convert(input) ||
1420
+ Integer.try_convert(input) ||
1421
+ String.try_convert(input) ||
1422
+ input
1423
+ end
1424
+
1425
+ def range_to_tuple(range)
1426
+ first = to_tuple_int(range.begin || 1)
1427
+ last = to_tuple_int(range.end || :*)
1428
+ last -= 1 if range.exclude_end? && range.end && last != STAR_INT
1429
+ unless first <= last
1430
+ raise DataFormatError, "invalid range for sequence-set: %p" % [range]
1431
+ end
1432
+ [first, last]
1433
+ end
1434
+
1435
+ def to_tuple_int(obj) STARS.include?(obj) ? STAR_INT : nz_number(obj) end
1436
+ def from_tuple_int(num) num == STAR_INT ? :* : num end
1437
+
1438
+ def tuple_to_str(tuple) tuple.uniq.map{ from_tuple_int _1 }.join(":") end
1439
+ def str_to_tuples(str) str.split(",", -1).map! { str_to_tuple _1 } end
1440
+ def str_to_tuple(str)
1441
+ raise DataFormatError, "invalid sequence set string" if str.empty?
1442
+ str.split(":", 2).map! { to_tuple_int _1 }.minmax
1443
+ end
1444
+
1445
+ def include_tuple?((min, max)) range_gte_to(min)&.cover?(min..max) end
1446
+
1447
+ def intersect_tuple?((min, max))
1448
+ range = range_gte_to(min) and
1449
+ range.include?(min) || range.include?(max) || (min..max).cover?(range)
1450
+ end
1451
+
1452
+ def modifying!
1453
+ if frozen?
1454
+ raise FrozenError, "can't modify frozen #{self.class}: %p" % [self]
1455
+ end
1456
+ end
1457
+
1458
+ def tuples_add(tuples) tuples.each do tuple_add _1 end; self end
1459
+ def tuples_subtract(tuples) tuples.each do tuple_subtract _1 end; self end
1460
+
1461
+ #
1462
+ # --|=====| |=====new tuple=====| append
1463
+ # ?????????-|=====new tuple=====|-|===lower===|-- insert
1464
+ #
1465
+ # |=====new tuple=====|
1466
+ # ---------??=======lower=======??--------------- noop
1467
+ #
1468
+ # ---------??===lower==|--|==| join remaining
1469
+ # ---------??===lower==|--|==|----|===upper===|-- join until upper
1470
+ # ---------??===lower==|--|==|--|=====upper===|-- join to upper
1471
+ def tuple_add(tuple)
1472
+ modifying!
1473
+ min, max = tuple
1474
+ lower, lower_idx = tuple_gte_with_index(min - 1)
1475
+ if lower.nil? then tuples << [min, max]
1476
+ elsif (max + 1) < lower.first then tuples.insert(lower_idx, [min, max])
1477
+ else tuple_coalesce(lower, lower_idx, min, max)
1478
+ end
1479
+ end
1480
+
1481
+ def tuple_coalesce(lower, lower_idx, min, max)
1482
+ return if lower.first <= min && max <= lower.last
1483
+ lower[0] = [min, lower.first].min
1484
+ lower[1] = [max, lower.last].max
1485
+ lower_idx += 1
1486
+ return if lower_idx == tuples.count
1487
+ tmax_adj = lower.last + 1
1488
+ upper, upper_idx = tuple_gte_with_index(tmax_adj)
1489
+ if upper
1490
+ tmax_adj < upper.first ? (upper_idx -= 1) : (lower[1] = upper.last)
1491
+ end
1492
+ tuples.slice!(lower_idx..upper_idx)
1493
+ end
1494
+
1495
+ # |====tuple================|
1496
+ # --|====| no more 1. noop
1497
+ # --|====|---------------------------|====lower====|-- 2. noop
1498
+ # -------|======lower================|---------------- 3. split
1499
+ # --------|=====lower================|---------------- 4. trim beginning
1500
+ #
1501
+ # -------|======lower====????????????----------------- trim lower
1502
+ # --------|=====lower====????????????----------------- delete lower
1503
+ #
1504
+ # -------??=====lower===============|----------------- 5. trim/delete one
1505
+ # -------??=====lower====|--|====| no more 6. delete rest
1506
+ # -------??=====lower====|--|====|---|====upper====|-- 7. delete until
1507
+ # -------??=====lower====|--|====|--|=====upper====|-- 8. delete and trim
1508
+ def tuple_subtract(tuple)
1509
+ modifying!
1510
+ min, max = tuple
1511
+ lower, idx = tuple_gte_with_index(min)
1512
+ if lower.nil? then nil # case 1.
1513
+ elsif max < lower.first then nil # case 2.
1514
+ elsif max < lower.last then tuple_trim_or_split lower, idx, min, max
1515
+ else tuples_trim_or_delete lower, idx, min, max
1516
+ end
1517
+ end
1518
+
1519
+ def tuple_trim_or_split(lower, idx, tmin, tmax)
1520
+ if lower.first < tmin # split
1521
+ tuples.insert(idx, [lower.first, tmin - 1])
1522
+ end
1523
+ lower[0] = tmax + 1
1524
+ end
1525
+
1526
+ def tuples_trim_or_delete(lower, lower_idx, tmin, tmax)
1527
+ if lower.first < tmin # trim lower
1528
+ lower[1] = tmin - 1
1529
+ lower_idx += 1
1530
+ end
1531
+ if tmax == lower.last # case 5
1532
+ upper_idx = lower_idx
1533
+ elsif (upper, upper_idx = tuple_gte_with_index(tmax + 1))
1534
+ upper_idx -= 1 # cases 7 and 8
1535
+ upper[0] = tmax + 1 if upper.first <= tmax # case 8 (else case 7)
1536
+ end
1537
+ tuples.slice!(lower_idx..upper_idx)
1538
+ end
1539
+
1540
+ def tuple_gte_with_index(num)
1541
+ idx = tuples.bsearch_index { _2 >= num } and [tuples[idx], idx]
1542
+ end
1543
+
1544
+ def range_gte_to(num)
1545
+ first, last = tuples.bsearch { _2 >= num }
1546
+ first..last if first
1547
+ end
1548
+
1549
+ def nz_number(num)
1550
+ String === num && !/\A[1-9]\d*\z/.match?(num) and
1551
+ raise DataFormatError, "%p is not a valid nz-number" % [num]
1552
+ NumValidator.ensure_nz_number Integer num
1553
+ rescue TypeError # To catch errors from Integer()
1554
+ raise DataFormatError, $!.message
1555
+ end
1556
+
1557
+ # intentionally defined after the class implementation
1558
+
1559
+ EMPTY = new.freeze
1560
+ FULL = self["1:*"]
1561
+ private_constant :EMPTY, :FULL
1562
+
1563
+ end
1564
+ end
1565
+ end