net-imap 0.5.6 → 0.6.0

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.
@@ -17,6 +17,39 @@ module Net
17
17
  class DataFormatError < Error
18
18
  end
19
19
 
20
+ # Error raised when the socket cannot be read, due to a Config limit.
21
+ class ResponseReadError < Error
22
+ end
23
+
24
+ # Error raised when a response is larger than IMAP#max_response_size.
25
+ class ResponseTooLargeError < ResponseReadError
26
+ attr_reader :bytes_read, :literal_size
27
+ attr_reader :max_response_size
28
+
29
+ def initialize(msg = nil, *args,
30
+ bytes_read: nil,
31
+ literal_size: nil,
32
+ max_response_size: nil,
33
+ **kwargs)
34
+ @bytes_read = bytes_read
35
+ @literal_size = literal_size
36
+ @max_response_size = max_response_size
37
+ msg ||= [
38
+ "Response size", response_size_msg, "exceeds max_response_size",
39
+ max_response_size && "(#{max_response_size}B)",
40
+ ].compact.join(" ")
41
+ super(msg, *args, **kwargs)
42
+ end
43
+
44
+ private
45
+
46
+ def response_size_msg
47
+ if bytes_read && literal_size
48
+ "(#{bytes_read}B read + #{literal_size}B literal)"
49
+ end
50
+ end
51
+ end
52
+
20
53
  # Error raised when a response from the server is non-parsable.
21
54
  class ResponseParseError < Error
22
55
  end
@@ -25,6 +25,12 @@ module Net
25
25
  # Some search extensions may result in the server sending ESearchResult
26
26
  # responses after the initiating command has completed. Use
27
27
  # IMAP#add_response_handler to handle these responses.
28
+ #
29
+ # ==== Compatibility with SearchResult
30
+ #
31
+ # Note that both SearchResult and ESearchResult implement +each+, +to_a+,
32
+ # and +to_sequence_set+. These methods can be used regardless of whether
33
+ # the server returns +SEARCH+ or +ESEARCH+ data (or no data).
28
34
  class ESearchResult < Data.define(:tag, :uid, :data)
29
35
  def initialize(tag: nil, uid: nil, data: nil)
30
36
  tag => String | nil; tag = -tag if tag
@@ -39,12 +45,49 @@ module Net
39
45
  # numbers or UIDs, +to_a+ returns that set as an array of integers.
40
46
  #
41
47
  # When both #all and #partial are +nil+, either because the server
42
- # returned no results or because +ALL+ and +PARTIAL+ were not included in
43
- # the IMAP#search +RETURN+ options, #to_a returns an empty array.
48
+ # returned no results or because neither +ALL+ or +PARTIAL+ were included
49
+ # in the IMAP#search +RETURN+ options, #to_a returns an empty array.
44
50
  #
45
51
  # Note that SearchResult also implements +to_a+, so it can be used without
46
52
  # checking if the server returned +SEARCH+ or +ESEARCH+ data.
47
- def to_a; all&.numbers || partial&.to_a || [] end
53
+ #
54
+ # Related: #each, #to_sequence_set, #all, #partial
55
+ def to_a; to_sequence_set.numbers end
56
+
57
+ # :call-seq: to_sequence_set -> SequenceSet or nil
58
+ #
59
+ # When either #all or #partial contains a SequenceSet of message sequence
60
+ # numbers or UIDs, +to_sequence_set+ returns that sequence set.
61
+ #
62
+ # When both #all and #partial are +nil+, either because the server
63
+ # returned no results or because neither +ALL+ or +PARTIAL+ were included
64
+ # in the IMAP#search +RETURN+ options, #to_sequence_set returns
65
+ # SequenceSet.empty.
66
+ #
67
+ # Note that SearchResult also implements +to_sequence_set+, so it can be
68
+ # used without checking if the server returned +SEARCH+ or +ESEARCH+ data.
69
+ #
70
+ # Related: #each, #to_a, #all, #partial
71
+ def to_sequence_set
72
+ all || partial&.to_sequence_set || SequenceSet.empty
73
+ end
74
+
75
+ # When either #all or #partial contains a SequenceSet of message sequence
76
+ # numbers or UIDs, +each+ yields each integer in the set.
77
+ #
78
+ # When both #all and #partial are +nil+, either because the server
79
+ # returned no results or because +ALL+ and +PARTIAL+ were not included in
80
+ # the IMAP#search +RETURN+ options, #each does not yield.
81
+ #
82
+ # Note that SearchResult also implements +#each+, so it can be used
83
+ # without checking if the server returned +SEARCH+ or +ESEARCH+ data.
84
+ #
85
+ # Related: #to_sequence_set, #to_a, #all, #partial
86
+ def each(&)
87
+ return to_enum(__callee__) unless block_given?
88
+ to_sequence_set.each_number(&)
89
+ self
90
+ end
48
91
 
49
92
  ##
50
93
  # attr_reader: tag
@@ -161,6 +204,8 @@ module Net
161
204
  #
162
205
  # See also: ESearchResult#to_a.
163
206
  def to_a; results&.numbers || [] end
207
+
208
+ alias to_sequence_set results
164
209
  end
165
210
 
166
211
  # :call-seq: partial -> PartialResult or nil
@@ -173,7 +173,7 @@ module Net
173
173
  SUBSCRIBED = :Subscribed
174
174
 
175
175
  # The mailbox is a remote mailbox.
176
- REMOTE = :Remove
176
+ REMOTE = :Remote
177
177
 
178
178
  # Alias for NO_INFERIORS, to match the \IMAP spelling.
179
179
  NOINFERIORS = NO_INFERIORS
@@ -6,8 +6,6 @@ module Net
6
6
  autoload :FetchData, "#{__dir__}/fetch_data"
7
7
  autoload :UIDFetchData, "#{__dir__}/fetch_data"
8
8
  autoload :SearchResult, "#{__dir__}/search_result"
9
- autoload :SequenceSet, "#{__dir__}/sequence_set"
10
- autoload :UIDPlusData, "#{__dir__}/uidplus_data"
11
9
  autoload :AppendUIDData, "#{__dir__}/uidplus_data"
12
10
  autoload :CopyUIDData, "#{__dir__}/uidplus_data"
13
11
  autoload :VanishedData, "#{__dir__}/vanished_data"
@@ -261,8 +259,8 @@ module Net
261
259
  #
262
260
  # === +UIDPLUS+ extension
263
261
  # See {[RFC4315 §3]}[https://www.rfc-editor.org/rfc/rfc4315#section-3].
264
- # * +APPENDUID+, #data is UIDPlusData. See IMAP#append.
265
- # * +COPYUID+, #data is UIDPlusData. See IMAP#copy.
262
+ # * +APPENDUID+, #data is AppendUIDData. See IMAP#append.
263
+ # * +COPYUID+, #data is CopyUIDData. See IMAP#copy.
266
264
  # * +UIDNOTSTICKY+, #data is +nil+. See IMAP#select.
267
265
  #
268
266
  # === +SEARCHRES+ extension
@@ -2017,24 +2017,19 @@ module Net
2017
2017
  CopyUID(validity, src_uids, dst_uids)
2018
2018
  end
2019
2019
 
2020
- def AppendUID(...) DeprecatedUIDPlus(...) || AppendUIDData.new(...) end
2021
- def CopyUID(...) DeprecatedUIDPlus(...) || CopyUIDData.new(...) end
2022
-
2023
2020
  # TODO: remove this code in the v0.6.0 release
2024
2021
  def DeprecatedUIDPlus(validity, src_uids = nil, dst_uids)
2025
2022
  return unless config.parser_use_deprecated_uidplus_data
2026
- compact_uid_sets = [src_uids, dst_uids].compact
2027
- count = compact_uid_sets.map { _1.count_with_duplicates }.max
2028
- max = config.parser_max_deprecated_uidplus_data_size
2029
- if count <= max
2030
- src_uids &&= src_uids.each_ordered_number.to_a
2031
- dst_uids = dst_uids.each_ordered_number.to_a
2032
- UIDPlusData.new(validity, src_uids, dst_uids)
2033
- elsif config.parser_use_deprecated_uidplus_data != :up_to_max_size
2034
- parse_error("uid-set is too large: %d > %d", count, max)
2035
- end
2023
+ warn("#{Config}#parser_use_deprecated_uidplus_data is ignored " \
2024
+ "since v0.6.0. Disable this warning by setting " \
2025
+ "config.parser_use_deprecated_uidplus_data = false.",
2026
+ category: :deprecated, uplevel: 9)
2027
+ nil
2036
2028
  end
2037
2029
 
2030
+ def AppendUID(...) DeprecatedUIDPlus(...) || AppendUIDData.new(...) end
2031
+ def CopyUID(...) DeprecatedUIDPlus(...) || CopyUIDData.new(...) end
2032
+
2038
2033
  ADDRESS_REGEXP = /\G
2039
2034
  \( (?: NIL | #{Patterns::QUOTED_rev2} ) # 1: NAME
2040
2035
  \s (?: NIL | #{Patterns::QUOTED_rev2} ) # 2: ROUTE
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+ # See https://www.rfc-editor.org/rfc/rfc9051#section-2.2.2
6
+ class ResponseReader # :nodoc:
7
+ attr_reader :client
8
+
9
+ def initialize(client, sock)
10
+ @client, @sock = client, sock
11
+ end
12
+
13
+ def read_response_buffer
14
+ @buff = String.new
15
+ catch :eof do
16
+ while true
17
+ read_line
18
+ break unless (@literal_size = get_literal_size)
19
+ read_literal
20
+ end
21
+ end
22
+ buff
23
+ ensure
24
+ @buff = nil
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :buff, :literal_size
30
+
31
+ def bytes_read = buff.bytesize
32
+ def empty? = buff.empty?
33
+ def done? = line_done? && !get_literal_size
34
+ def line_done? = buff.end_with?(CRLF)
35
+ def get_literal_size = /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i
36
+
37
+ def read_line
38
+ buff << (@sock.gets(CRLF, read_limit) or throw :eof)
39
+ max_response_remaining! unless line_done?
40
+ end
41
+
42
+ def read_literal
43
+ # check before allocating memory for literal
44
+ max_response_remaining!
45
+ literal = String.new(capacity: literal_size)
46
+ buff << (@sock.read(read_limit(literal_size), literal) or throw :eof)
47
+ ensure
48
+ @literal_size = nil
49
+ end
50
+
51
+ def read_limit(limit = nil)
52
+ [limit, max_response_remaining!].compact.min
53
+ end
54
+
55
+ def max_response_size = client.max_response_size
56
+ def max_response_remaining = max_response_size &.- bytes_read
57
+ def response_too_large? = max_response_size &.< min_response_size
58
+ def min_response_size = bytes_read + min_response_remaining
59
+
60
+ def min_response_remaining
61
+ empty? ? 3 : done? ? 0 : (literal_size || 0) + 2
62
+ end
63
+
64
+ def max_response_remaining!
65
+ return max_response_remaining unless response_too_large?
66
+ raise ResponseTooLargeError.new(
67
+ max_response_size:, bytes_read:, literal_size:,
68
+ )
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -7,6 +7,12 @@ module Net
7
7
  # identifiers returned by Net::IMAP#uid_search.
8
8
  #
9
9
  # For backward compatibility, SearchResult inherits from Array.
10
+ #
11
+ # ==== Compatibility with ESearchResult
12
+ #
13
+ # Note that both SearchResult and ESearchResult implement +each+, +to_a+,
14
+ # and +to_sequence_set+. These methods can be used regardless of whether
15
+ # the server returns +SEARCH+ or +ESEARCH+ data (or no data).
10
16
  class SearchResult < Array
11
17
 
12
18
  # Returns a SearchResult populated with the given +seq_nums+.
@@ -60,9 +66,8 @@ module Net
60
66
  # [3, 5, 7] == Net::IMAP::SearchResult[3, 5, 7, modseq: 99] # => true
61
67
  #
62
68
  def ==(other)
63
- (modseq ?
64
- other.is_a?(self.class) && modseq == other.modseq :
65
- other.is_a?(Array)) &&
69
+ other.is_a?(Array) &&
70
+ modseq == (other.modseq if other.respond_to?(:modseq)) &&
66
71
  size == other.size &&
67
72
  sort == other.sort
68
73
  end