net-imap 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc6f6d65e2f5092b38316ed4d381551838dd412a5b376c06359225e8f53a020c
4
- data.tar.gz: d5daf47c70a868650687f7db3d1a1e074555a634708c16792e19f1cb53f8cc83
3
+ metadata.gz: ffcec5e3cc42d7f72c63520110ec442f760c35472401de495001f420cfa059c5
4
+ data.tar.gz: aedd703997fc651cb6c67635a3d20a4159720c6e1bda41c07322f2bd67a627d6
5
5
  SHA512:
6
- metadata.gz: 1aeb682e73a666591079aac75c4325f22079dfbf61dbe71df135059b9087890633c31fd036e61757a217f1b1ff3e1d3771801a7ec6f3817646a0f8d2c0896386
7
- data.tar.gz: 8cd118d9943b83010dd2f4916ab7641e120743991d3bc06062c4eb286098d1af8af05d86854b3e44235a0b3263b801db1cf9a7d64ac3140d89eda1aca086e6cd
6
+ metadata.gz: 1fa95302f29607d152b991535a9d14cda5dae1a1fce06b1d831dd2b09f69b2fd845367e0ebed73dd4e79d04c064a801e59865e76947060a4811304728458f3c7
7
+ data.tar.gz: 2dc9a56758d6f370affc9540a8ca26b8ad866ca5ef0e5b9bc2c30ff6257e3461e5ed23173e1c5e98a2e95c1e0f0a844c96f9e34d41151e9196ab513ac6987d33
data/.document ADDED
@@ -0,0 +1,3 @@
1
+ *.rdoc
2
+ *.md
3
+ lib
data/.rdoc_options ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ charset: UTF-8
3
+ main_page: README.md
4
+ markup: rdoc
5
+ title: net-imap # rake task override's title to add the version number
6
+ op_dir: doc
7
+ # vim:ft=yaml
data/Gemfile CHANGED
@@ -11,7 +11,7 @@ gem "psych", ">= 5.3.0" # 5.2.5 for Data serialization, 5.3.0 for TruffleRuby
11
11
 
12
12
  gem "irb"
13
13
  gem "rake"
14
- gem "rdoc"
14
+ gem "rdoc", ">= 7.2.0"
15
15
  gem "test-unit"
16
16
  gem "test-unit-ruby-core", git: "https://github.com/ruby/test-unit-ruby-core"
17
17
 
data/README.md CHANGED
@@ -61,9 +61,9 @@ imap.search(["BEFORE", "30-Apr-2003", "SINCE", "1-Apr-2003"]).each do |message_i
61
61
  else
62
62
  imap.copy(message_id, "Mail/sent-apr03")
63
63
  imap.store(message_id, "+FLAGS", [:Deleted])
64
+ imap.expunge
64
65
  end
65
66
  end
66
- imap.expunge
67
67
  ```
68
68
 
69
69
  ## Development
@@ -4,6 +4,8 @@ require "date"
4
4
 
5
5
  require_relative "errors"
6
6
 
7
+ # :enddoc:
8
+
7
9
  module Net
8
10
  class IMAP < Protocol
9
11
 
@@ -25,6 +27,7 @@ module Net
25
27
  end
26
28
  when Time, Date, DateTime
27
29
  when Symbol
30
+ Flag.validate(data)
28
31
  else
29
32
  data.validate
30
33
  end
@@ -45,7 +48,7 @@ module Net
45
48
  when Date
46
49
  send_date_data(data)
47
50
  when Symbol
48
- send_symbol_data(data)
51
+ Flag[data].send_data(self, tag)
49
52
  else
50
53
  data.send_data(self, tag)
51
54
  end
@@ -77,9 +80,23 @@ module Net
77
80
  put_string('"' + str.gsub(/["\\]/, "\\\\\\&") + '"')
78
81
  end
79
82
 
80
- def send_literal(str, tag = nil)
83
+ def send_binary_literal(*, **) = send_literal(*, **, binary: true)
84
+
85
+ # `non_sync` is an optional tri-state flag:
86
+ # * `true` -> Force non-synchronizing `LITERAL+`/`LITERAL-` behavior.
87
+ # TODO: raise or warn when capabilities don't allow non_sync.
88
+ # * `false` -> Force normal synchronizing literal behavior.
89
+ # * `nil` -> (default) Currently behaves like `false` (will be dynamic).
90
+ def send_literal(str, tag = nil, binary: false, non_sync: nil)
81
91
  synchronize do
82
- put_string("{" + str.bytesize.to_s + "}" + CRLF)
92
+ non_sync = non_sync_literal?(str.bytesize) if non_sync.nil?
93
+ prefix = "~" if binary
94
+ plus = "+" if non_sync
95
+ put_string("#{prefix}{#{str.bytesize}#{plus}}\r\n")
96
+ if non_sync
97
+ put_string(str)
98
+ return
99
+ end
83
100
  @continued_command_tag = tag
84
101
  @continuation_request_exception = nil
85
102
  begin
@@ -94,6 +111,13 @@ module Net
94
111
  end
95
112
  end
96
113
 
114
+ def non_sync_literal?(bytesize)
115
+ capabilities_cached? &&
116
+ bytesize <= config.max_non_synchronizing_literal &&
117
+ (capable?("LITERAL+") ||
118
+ bytesize <= 4096 && (capable?("IMAP4rev2") || capable?("LITERAL-")))
119
+ end
120
+
97
121
  def send_number_data(num)
98
122
  put_string(num.to_s)
99
123
  end
@@ -115,11 +139,13 @@ module Net
115
139
  def send_date_data(date) put_string Net::IMAP.encode_date(date) end
116
140
  def send_time_data(time) put_string Net::IMAP.encode_time(time) end
117
141
 
118
- def send_symbol_data(symbol)
119
- put_string("\\" + symbol.to_s)
120
- end
121
-
122
142
  CommandData = Data.define(:data) do # :nodoc:
143
+ def self.validate(...)
144
+ data = new(...)
145
+ data.validate
146
+ data
147
+ end
148
+
123
149
  def send_data(imap, tag)
124
150
  raise NoMethodError, "#{self.class} must implement #{__method__}"
125
151
  end
@@ -128,15 +154,109 @@ module Net
128
154
  end
129
155
  end
130
156
 
157
+ # Represents IMAP +text+ data, which may contain any 7-bit ASCII character,
158
+ # except for +NULL+, +CR+, or +LF+. +text+ is extended to allow any
159
+ # multibyte +UTF-8+ character when either +UTF8=ACCEPT+ or +IMAP4rev2+ have
160
+ # been enabled, or when the server supports only +IMAP4rev2+ and not earlier
161
+ # IMAP revisions, or when the server advertises +UTF8=ONLY+.
162
+ #
163
+ # NOTE: The current implementation does not validate whether the connection
164
+ # currently supports UTF-8. Future versions may change.
165
+ #
166
+ # The string's bytes must be valid ASCII or valid UTF-8. The string's
167
+ # reported encoding is ignored, but the string is _not_ transcoded.
168
+ class RawText < CommandData # :nodoc:
169
+ def initialize(data:)
170
+ data = String(data.to_str)
171
+ data = if data.encoding in Encoding::ASCII | Encoding::UTF_8
172
+ -data
173
+ elsif data.ascii_only?
174
+ -(data.dup.force_encoding("ASCII"))
175
+ else
176
+ -(data.dup.force_encoding("UTF-8"))
177
+ end
178
+ super
179
+ validate
180
+ end
181
+
182
+ def validate
183
+ if data.include?("\0")
184
+ raise DataFormatError, "NULL byte must be binary literal encoded"
185
+ elsif !data.valid_encoding?
186
+ raise DataFormatError, "invalid UTF-8 must be literal encoded"
187
+ elsif /[\r\n]/.match?(data)
188
+ raise DataFormatError, "CR and LF bytes must be literal encoded"
189
+ end
190
+ end
191
+
192
+ def ascii_only? = data.ascii_only?
193
+
194
+ def send_data(imap, tag) = imap.__send__(:put_string, data)
195
+ end
196
+
131
197
  class RawData < CommandData # :nodoc:
132
- def send_data(imap, tag)
133
- imap.__send__(:put_string, data)
198
+ def initialize(data:)
199
+ data = split_parts(data)
200
+ super
201
+ validate
202
+ end
203
+
204
+ def send_data(imap, tag) = data.each do _1.send_data(imap, tag) end
205
+
206
+ def validate
207
+ return unless data.last in RawText(data: text)
208
+ if text.rindex(/~?\{[1-9]\d*\+?\}\z/n)
209
+ raise DataFormatError, "RawData cannot end with literal continuation"
210
+ end
211
+ end
212
+
213
+ private
214
+
215
+ def split_parts(data)
216
+ data = data.b # dups and ensures BINARY encoding
217
+ parts = []
218
+ while data.match(/(~)?\{(0|[1-9]\d*)(\+)?\}\r\n/n)
219
+ text, binary, bytesize, non_sync, data = $`, !!$1, $2, !!$3, $'
220
+ bytesize = NumValidator.coerce_number64 bytesize
221
+ parts << RawText[text] unless text.empty?
222
+ parts << extract_literal(data, binary:, bytesize:, non_sync:)
223
+ data.bytesplice(0, bytesize, "")
224
+ end
225
+ parts << RawText[data] unless data.empty?
226
+ parts
227
+ end
228
+
229
+ def extract_literal(data, binary:, bytesize:, non_sync:)
230
+ if data.bytesize < bytesize
231
+ raise DataFormatError, "Too few bytes in string for literal, " \
232
+ "expected: %s, remaining: %s" % [bytesize, data.bytesize]
233
+ end
234
+ literal = data.byteslice(0, bytesize)
235
+ (binary ? Literal8 : Literal).new(data: literal, non_sync:)
134
236
  end
135
237
  end
136
238
 
137
239
  class Atom < CommandData # :nodoc:
240
+ def initialize(**)
241
+ super
242
+ validate
243
+ end
244
+
245
+ def validate
246
+ data.to_s.ascii_only? \
247
+ or raise DataFormatError, "#{self.class} must be ASCII only"
248
+ data.match?(ResponseParser::Patterns::ATOM_SPECIALS) \
249
+ and raise DataFormatError, "#{self.class} must not contain atom-specials"
250
+ end
251
+
252
+ def send_data(imap, tag)
253
+ imap.__send__(:put_string, data.to_s)
254
+ end
255
+ end
256
+
257
+ class Flag < Atom # :nodoc:
138
258
  def send_data(imap, tag)
139
- imap.__send__(:put_string, data)
259
+ imap.__send__(:put_string, "\\#{data}")
140
260
  end
141
261
  end
142
262
 
@@ -146,9 +266,39 @@ module Net
146
266
  end
147
267
  end
148
268
 
149
- class Literal < CommandData # :nodoc:
269
+ class Literal < Data.define(:data, :non_sync) # :nodoc:
270
+ def self.validate(...)
271
+ data = new(...)
272
+ data.validate
273
+ data
274
+ end
275
+
276
+ def initialize(data:, non_sync: nil)
277
+ data = -String(data.to_str).b or
278
+ raise DataFormatError, "#{self.class} expects string input"
279
+ super
280
+ validate
281
+ end
282
+
283
+ def bytesize = data.bytesize
284
+
285
+ def validate
286
+ if data.include?("\0")
287
+ raise DataFormatError, "NULL byte not allowed in #{self.class}. " \
288
+ "Use #{Literal8} or a null-safe encoding."
289
+ end
290
+ end
291
+
292
+ def send_data(imap, tag)
293
+ imap.__send__(:send_literal, data, tag, non_sync:)
294
+ end
295
+ end
296
+
297
+ class Literal8 < Literal # :nodoc:
298
+ def validate = nil # all bytes are okay
299
+
150
300
  def send_data(imap, tag)
151
- imap.__send__(:send_literal, data, tag)
301
+ imap.__send__(:send_binary_literal, data, tag, non_sync:)
152
302
  end
153
303
  end
154
304
 
@@ -221,6 +371,14 @@ module Net
221
371
 
222
372
  module_function
223
373
 
374
+ def literal_or_literal8(input, name: "argument")
375
+ return input if input in Literal | Literal8
376
+ data = String.try_convert(input) \
377
+ or raise TypeError, "expected #{name} to be String, got #{input.class}"
378
+ type = data.include?("\0") ? Literal8 : Literal
379
+ type.new(data:)
380
+ end
381
+
224
382
  # Allows symbols in addition to strings
225
383
  def valid_string?(str)
226
384
  str.is_a?(Symbol) || str.respond_to?(:to_str)
@@ -27,24 +27,23 @@ module Net
27
27
 
28
28
  def self.attr_accessor(name) # :nodoc: internal API
29
29
  name = name.to_sym
30
+ raise ArgumentError, "already defined #{name}" if attributes.include?(name)
31
+ attributes << name
30
32
  def_delegators :data, name, :"#{name}="
31
33
  end
32
34
 
33
- def self.attributes
34
- instance_methods.grep(/=\z/).map { _1.to_s.delete_suffix("=").to_sym }
35
- end
36
- private_class_method :attributes
35
+ # An array of Config attribute names
36
+ singleton_class.attr_reader :attributes
37
+ @attributes = []
37
38
 
38
39
  def self.struct # :nodoc: internal API
39
- unless defined?(self::Struct)
40
- const_set :Struct, Struct.new(*attributes)
41
- end
42
- self::Struct
40
+ attributes.freeze
41
+ Struct.new(*attributes)
43
42
  end
44
43
 
45
44
  def initialize # :notnew:
46
45
  super()
47
- @data = AttrAccessors.struct.new
46
+ @data = Config::Struct.new
48
47
  end
49
48
 
50
49
  # Freezes the internal attributes struct, in addition to +self+.
@@ -66,11 +66,61 @@ module Net
66
66
  # inherited, or +false+ if any of them are overriden. When no +attrs+
67
67
  # are given, returns +true+ if *all* attributes are inherited, or
68
68
  # +false+ if any attribute is overriden.
69
+ #
70
+ # Related: #overrides?
69
71
  def inherited?(*attrs)
70
72
  attrs = data.members if attrs.empty?
71
73
  attrs.all? { data[_1] == INHERITED }
72
74
  end
73
75
 
76
+ # :call-seq:
77
+ # inherits_defaults?(*attrs) -> true | Rational | nil | false
78
+ #
79
+ # Returns whether all +attrs+ are inherited from a default config.
80
+ # When no +attrs+ are given, returns whether *all* attributes are
81
+ # inherited from a default config.
82
+ #
83
+ # Returns +true+ when all attributes inherit from Config.default, the
84
+ # version number (as a Rational) when all attributes inherit from a
85
+ # versioned default (see Config@Versioned+defaults), +nil+ if any
86
+ # attributes inherit from Config.global overrides (but not from
87
+ # non-global ancestors), or +false+ when any attributes have been
88
+ # overridden by +self+ or an ancestor (besides global or default
89
+ # configs),
90
+ #
91
+ # Related: #overrides?
92
+ def inherits_defaults?(*attrs)
93
+ if equal?(Config.default)
94
+ true
95
+ elsif equal?(Config.global)
96
+ true if inherited?(*attrs)
97
+ elsif (v = AttrVersionDefaults::VERSIONS.find { equal? Config[_1] })
98
+ attrs = DEFAULT_TO_INHERIT if attrs.empty?
99
+ attrs &= DEFAULT_TO_INHERIT
100
+ (attrs.empty? || parent.inherits_defaults?(*attrs)) && v
101
+ else
102
+ inherited?(*attrs) && parent.inherits_defaults?(*attrs)
103
+ end
104
+ end
105
+
106
+ # :call-seq:
107
+ # overrides?(attr) -> true or false
108
+ # overrides?(*attrs) -> true or false
109
+ # overrides? -> true or false
110
+ #
111
+ # Returns +true+ if +attr+ is defined on this config and not inherited
112
+ # from #parent.
113
+ #
114
+ # When multiple +attrs+ are given, returns +true+ if
115
+ # *any* of them are defined on +self+. When no +attrs+ are given,
116
+ # returns +true+ if *any* attribute is overriden.
117
+ #
118
+ # Related: #inherited?
119
+ def overrides?(*attrs)
120
+ attrs = data.members if attrs.empty?
121
+ attrs.any? { data[_1] != INHERITED }
122
+ end
123
+
74
124
  # :call-seq:
75
125
  # reset -> self
76
126
  # reset(attr) -> attribute value
@@ -24,7 +24,7 @@ module Net
24
24
  VERSIONS = ((0.0r..FUTURE_VERSION) % 0.1r).to_a.freeze
25
25
 
26
26
  # See Config.version_defaults.
27
- singleton_class.attr_accessor :version_defaults
27
+ singleton_class.attr_reader :version_defaults
28
28
 
29
29
  @version_defaults = Hash.new {|h, k|
30
30
  # NOTE: String responds to both so the order is significant.
@@ -59,10 +59,6 @@ module Net
59
59
  end
60
60
 
61
61
  def self.compile_version_defaults!
62
- # Temporarily assign Config.default, enabling #load_defaults(:default)
63
- version_defaults[:default] = Config.default
64
- # Use #load_defaults so some attributes are inherited from global.
65
- version_defaults[:default] = Config.new.load_defaults(:default).freeze
66
62
  version_defaults[0.0r] = Config[version_defaults.fetch(0.0r)]
67
63
 
68
64
  VERSIONS.each_cons(2) do |prior, version|
@@ -81,6 +77,7 @@ module Net
81
77
 
82
78
  version_defaults[:original] = Config[0.0r]
83
79
  version_defaults[:current] = Config[CURRENT_VERSION]
80
+ version_defaults[:default] = Config[CURRENT_VERSION]
84
81
  version_defaults[:next] = Config[NEXT_VERSION]
85
82
  version_defaults[:future] = Config[FUTURE_VERSION]
86
83
 
@@ -281,6 +281,40 @@ module Net
281
281
  0.5r => true,
282
282
  }
283
283
 
284
+ # The maximum bytesize for sending non-synchronizing literals, when the
285
+ # server supports them. To disable non-synchronizing literals, set the
286
+ # value to +-1+.
287
+ #
288
+ # Non-synchronizing literals are only sent when the server's
289
+ # capabilities[rdoc-ref:IMAP#capabilities] have been
290
+ # cached[rdoc-ref:IMAP#capabilities_cached?] and include either
291
+ # <tt>LITERAL+</tt> [RFC7888[https://www.rfc-editor.org/rfc/rfc7888]],
292
+ # <tt>LITERAL-</tt> [RFC7888[https://www.rfc-editor.org/rfc/rfc7888]], or
293
+ # +IMAP4rev2+ [RFC9051[https://www.rfc-editor.org/rfc/rfc9051]].
294
+ #
295
+ # For <tt>LITERAL+</tt>, this value is the only limit on whether a literal
296
+ # value is sent as non-synchronizing literals. For <tt>LITERAL-</tt> and
297
+ # <tt>IMAP4rev2</tt>, non-synchronizing literals must also be smaller than
298
+ # +4096+ bytes.
299
+ #
300
+ # Non-synchronizing literals avoid the latency of waiting for the server
301
+ # to allow continuation. However, if a client sends a non-synchronizing
302
+ # literal that is too large for the server, the server may need to close
303
+ # the connection. Because <tt>LITERAL+</tt> does not directly indicate
304
+ # the server's limits, it's best to avoid sending very large
305
+ # non-synchronized literals.
306
+ #
307
+ # ==== Versioned Defaults
308
+ #
309
+ # max_non_synchronizing_literal <em>was added in +v0.6.4+.</em>
310
+ #
311
+ # * original: +-1+ (_never_ send non-synchronizing literals)
312
+ # * +0.6+: 16 KiB
313
+ attr_accessor :max_non_synchronizing_literal, type: Integer?, defaults: {
314
+ 0.0r => -1,
315
+ 0.6r => 16 << 16, # 16 KiB
316
+ }
317
+
284
318
  # The maximum allowed server response size. When +nil+, there is no limit
285
319
  # on response size.
286
320
  #
@@ -578,7 +612,6 @@ module Net
578
612
  if equal? Config.default then "#{Config}.default"
579
613
  elsif equal? Config.global then "#{Config}.global"
580
614
  elsif equal? Config[0.0r] then "#{Config}[:original]"
581
- elsif equal? Config[:default] then "#{Config}[:default]"
582
615
  elsif (v = AttrVersionDefaults::VERSIONS.find { equal? Config[_1] })
583
616
  "%s[%0.1f]" % [Config, v]
584
617
  else
@@ -631,6 +664,7 @@ module Net
631
664
  to_h.reject {|k,v| DEFAULT_TO_INHERIT.include?(k) }
632
665
  end
633
666
 
667
+ Struct = AttrAccessors.struct
634
668
  @default = AttrVersionDefaults.compile_default!
635
669
  @global = default.new
636
670
  AttrVersionDefaults.compile_version_defaults!
@@ -174,6 +174,22 @@ module Net
174
174
  0 < num && num <= 0xffff_ffff
175
175
  end
176
176
 
177
+ # Check if argument is a valid 'number64' according to RFC 9051
178
+ # number64 = 1*DIGIT
179
+ # ; Unsigned 63-bit integer
180
+ # ; (0 <= n <= 9,223,372,036,854,775,807)
181
+ def valid_number64?(num)
182
+ 0 <= num && num <= 0x7fff_ffff_ffff_ffff
183
+ end
184
+
185
+ # Check if argument is a valid 'number64' according to RFC 9051
186
+ # nz-number64 = digit-nz *DIGIT
187
+ # ; Unsigned 63-bit integer
188
+ # ; (0 < n <= 9,223,372,036,854,775,807)
189
+ def valid_nz_number64?(num)
190
+ 0 < num && num <= 0x7fff_ffff_ffff_ffff
191
+ end
192
+
177
193
  # Check if argument is a valid 'mod-sequence-value' according to RFC 4551
178
194
  # mod-sequence-value = 1*DIGIT
179
195
  # ; Positive unsigned 64-bit integer
@@ -203,6 +219,20 @@ module Net
203
219
  "nz-number must be non-zero unsigned 32-bit integer: #{num}"
204
220
  end
205
221
 
222
+ # Ensure argument is 'number64' or raise DataFormatError
223
+ def ensure_number64(num)
224
+ return num if valid_number64?(num)
225
+ raise DataFormatError,
226
+ "number64 must be unsigned 63-bit integer: #{num}"
227
+ end
228
+
229
+ # Ensure argument is 'nz-number64' or raise DataFormatError
230
+ def ensure_nz_number64(num)
231
+ return num if valid_nz_number64?(num)
232
+ raise DataFormatError,
233
+ "nz-number64 must be non-zero unsigned 63-bit integer: #{num}"
234
+ end
235
+
206
236
  # Ensure argument is 'mod-sequence-value' or raise DataFormatError
207
237
  def ensure_mod_sequence_value(num)
208
238
  return num if valid_mod_sequence_value?(num)
@@ -237,6 +267,26 @@ module Net
237
267
  end
238
268
  end
239
269
 
270
+ # Like #ensure_number64, but usable with numeric String input.
271
+ def coerce_number64(num)
272
+ case num
273
+ when Integer then ensure_number64 num
274
+ when NUMBER_RE then ensure_number64 Integer num
275
+ else
276
+ raise DataFormatError, "%p is not a valid number64" % [num]
277
+ end
278
+ end
279
+
280
+ # Like #ensure_nz_number64, but usable with numeric String input.
281
+ def coerce_nz_number64(num)
282
+ case num
283
+ when Integer then ensure_nz_number64 num
284
+ when NUMBER_RE then ensure_nz_number64 Integer num
285
+ else
286
+ raise DataFormatError, "%p is not a valid nz-number64" % [num]
287
+ end
288
+ end
289
+
240
290
  # Like #ensure_mod_sequence_value, but usable with numeric String input.
241
291
  def coerce_mod_sequence_value(num)
242
292
  case num