net-imap 0.5.1 → 0.5.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/README.md +10 -4
- data/docs/styles.css +75 -14
- data/lib/net/imap/command_data.rb +48 -46
- data/lib/net/imap/config.rb +73 -3
- data/lib/net/imap/data_lite.rb +226 -0
- data/lib/net/imap/esearch_result.rb +180 -0
- data/lib/net/imap/fetch_data.rb +126 -47
- data/lib/net/imap/response_data.rb +118 -144
- data/lib/net/imap/response_parser/parser_utils.rb +5 -0
- data/lib/net/imap/response_parser.rb +178 -17
- data/lib/net/imap/sasl/anonymous_authenticator.rb +3 -3
- data/lib/net/imap/sasl/cram_md5_authenticator.rb +3 -3
- data/lib/net/imap/sasl/digest_md5_authenticator.rb +8 -8
- data/lib/net/imap/sasl/external_authenticator.rb +2 -2
- data/lib/net/imap/sasl/gs2_header.rb +7 -7
- data/lib/net/imap/sasl/login_authenticator.rb +2 -2
- data/lib/net/imap/sasl/oauthbearer_authenticator.rb +6 -6
- data/lib/net/imap/sasl/plain_authenticator.rb +7 -7
- data/lib/net/imap/sasl/scram_authenticator.rb +8 -8
- data/lib/net/imap/sasl.rb +1 -1
- data/lib/net/imap/search_result.rb +2 -2
- data/lib/net/imap/sequence_set.rb +193 -58
- data/lib/net/imap/stringprep/nameprep.rb +1 -1
- data/lib/net/imap/stringprep/trace.rb +4 -4
- data/lib/net/imap/uidplus_data.rb +244 -0
- data/lib/net/imap/vanished_data.rb +56 -0
- data/lib/net/imap.rb +694 -319
- data/rakelib/rfcs.rake +2 -0
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cdbdda0ed73da899ec338f66022a16104562d3701c568b0a6d4897270a608ac5
|
4
|
+
data.tar.gz: b6a7ec70776b32f8eb57d01a0869503eb5d76f719ac091eb92e0206608e936e9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 381bf2428719ed8decb5d241fda0e19f28031dd4a77980b3717bb29c37bed1c927f00e5b57862e209ecf24b2e9b38c01088d6e1a90fc4b4cc026cdd9e6611100
|
7
|
+
data.tar.gz: 513c6a77d46b6d2cf67aea4511023acc76c69940e3b1a0d0eae7223b53ff63bc8e6e009f51fef826b09f76f6ad1d92e84243e8457f59d3912db7e74bf69d3d1b
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
# Net::IMAP
|
2
2
|
|
3
3
|
Net::IMAP implements Internet Message Access Protocol (IMAP) client
|
4
|
-
functionality. The protocol is described in
|
4
|
+
functionality. The protocol is described in
|
5
|
+
[RFC3501](https://www.rfc-editor.org/rfc/rfc3501),
|
6
|
+
[RFC9051](https://www.rfc-editor.org/rfc/rfc9051) and various extensions.
|
5
7
|
|
6
8
|
## Installation
|
7
9
|
|
@@ -50,12 +52,16 @@ end
|
|
50
52
|
|
51
53
|
```ruby
|
52
54
|
imap.select('Mail/sent-mail')
|
53
|
-
if
|
55
|
+
if imap.list('Mail/', 'sent-apr03').empty?
|
54
56
|
imap.create('Mail/sent-apr03')
|
55
57
|
end
|
56
58
|
imap.search(["BEFORE", "30-Apr-2003", "SINCE", "1-Apr-2003"]).each do |message_id|
|
57
|
-
imap.
|
58
|
-
|
59
|
+
if imap.capable?(:move) || imap.capable?(:IMAP4rev2)
|
60
|
+
imap.move(message_id, "Mail/sent-apr03")
|
61
|
+
else
|
62
|
+
imap.copy(message_id, "Mail/sent-apr03")
|
63
|
+
imap.store(message_id, "+FLAGS", [:Deleted])
|
64
|
+
end
|
59
65
|
end
|
60
66
|
imap.expunge
|
61
67
|
```
|
data/docs/styles.css
CHANGED
@@ -1,24 +1,85 @@
|
|
1
1
|
/* this is a work in progress. :) */
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
/***********************************************
|
4
|
+
* Method descriptions
|
5
|
+
***********************************************/
|
6
|
+
|
7
|
+
main .method-detail {
|
8
|
+
display: grid;
|
9
|
+
grid-template-columns: 1fr auto;
|
10
|
+
justify-content: space-between;
|
11
|
+
}
|
12
|
+
|
13
|
+
main .method-header,
|
14
|
+
main .method-controls,
|
15
|
+
.attribute-method-heading {
|
6
16
|
padding: 0.5em;
|
7
|
-
border
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
17
|
+
/* border: 1px solid var(--highlight-color); */
|
18
|
+
background: var(--table-header-background-color);
|
19
|
+
line-height: 1.6;
|
20
|
+
}
|
21
|
+
|
22
|
+
.attribute-method-heading .attribute-access-type {
|
23
|
+
float: right;
|
24
|
+
}
|
25
|
+
|
26
|
+
main .method-header {
|
27
|
+
border-right: none;
|
28
|
+
border-radius: 4px 0 0 4px;
|
29
|
+
}
|
30
|
+
|
31
|
+
main .method-heading :any-link {
|
32
|
+
text-decoration: none;
|
33
|
+
}
|
34
|
+
|
35
|
+
main .method-controls {
|
36
|
+
border-left: none;
|
37
|
+
border-radius: 0 4px 4px 0;
|
12
38
|
}
|
13
39
|
|
14
40
|
main .method-description, main .aliases {
|
41
|
+
grid-column: 1 / span 2;
|
15
42
|
padding-left: 1em;
|
16
43
|
}
|
17
44
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
45
|
+
@media (max-width: 700px) {
|
46
|
+
main .method-header, main .method-controls, main .method-description {
|
47
|
+
grid-column: 1 / span 2;
|
48
|
+
margin: 0;
|
49
|
+
}
|
50
|
+
main .method-controls {
|
51
|
+
background: none;
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
/***********************************************
|
56
|
+
* Description lists
|
57
|
+
***********************************************/
|
58
|
+
|
59
|
+
main dt {
|
60
|
+
margin-bottom: 0; /* override rdoc 6.8 */
|
61
|
+
float: unset; /* override rdoc 6.8 */
|
62
|
+
line-height: 1.5; /* matches `main p` */
|
63
|
+
}
|
64
|
+
|
65
|
+
main dl.note-list dt {
|
66
|
+
margin-right: 1em;
|
67
|
+
float: left;
|
68
|
+
}
|
69
|
+
|
70
|
+
main dl.note-list dt:has(+ dt) {
|
71
|
+
margin-right: 0.25em;
|
72
|
+
}
|
73
|
+
|
74
|
+
main dl.note-list dt:has(+ dt)::after {
|
75
|
+
content: ', ';
|
76
|
+
font-weight: normal;
|
77
|
+
}
|
78
|
+
|
79
|
+
main dd {
|
80
|
+
margin: 0 0 1em 1em;
|
81
|
+
}
|
82
|
+
|
83
|
+
main dd p:first-child {
|
84
|
+
margin-top: 0;
|
24
85
|
}
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require "date"
|
4
4
|
|
5
5
|
require_relative "errors"
|
6
|
+
require_relative "data_lite"
|
6
7
|
|
7
8
|
module Net
|
8
9
|
class IMAP < Protocol
|
@@ -119,80 +120,85 @@ module Net
|
|
119
120
|
put_string("\\" + symbol.to_s)
|
120
121
|
end
|
121
122
|
|
122
|
-
|
123
|
+
CommandData = Data.define(:data) do # :nodoc:
|
123
124
|
def send_data(imap, tag)
|
124
|
-
|
125
|
+
raise NoMethodError, "#{self.class} must implement #{__method__}"
|
125
126
|
end
|
126
127
|
|
127
128
|
def validate
|
128
129
|
end
|
129
|
-
|
130
|
-
private
|
131
|
-
|
132
|
-
def initialize(data)
|
133
|
-
@data = data
|
134
|
-
end
|
135
130
|
end
|
136
131
|
|
137
|
-
class
|
132
|
+
class RawData < CommandData # :nodoc:
|
138
133
|
def send_data(imap, tag)
|
139
|
-
imap.__send__(:put_string,
|
140
|
-
end
|
141
|
-
|
142
|
-
def validate
|
143
|
-
end
|
144
|
-
|
145
|
-
private
|
146
|
-
|
147
|
-
def initialize(data)
|
148
|
-
@data = data
|
134
|
+
imap.__send__(:put_string, data)
|
149
135
|
end
|
150
136
|
end
|
151
137
|
|
152
|
-
class
|
138
|
+
class Atom < CommandData # :nodoc:
|
153
139
|
def send_data(imap, tag)
|
154
|
-
imap.__send__(:
|
155
|
-
end
|
156
|
-
|
157
|
-
def validate
|
140
|
+
imap.__send__(:put_string, data)
|
158
141
|
end
|
142
|
+
end
|
159
143
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
@data = data
|
144
|
+
class QuotedString < CommandData # :nodoc:
|
145
|
+
def send_data(imap, tag)
|
146
|
+
imap.__send__(:send_quoted_string, data)
|
164
147
|
end
|
165
148
|
end
|
166
149
|
|
167
|
-
class Literal # :nodoc:
|
150
|
+
class Literal < CommandData # :nodoc:
|
168
151
|
def send_data(imap, tag)
|
169
|
-
imap.__send__(:send_literal,
|
152
|
+
imap.__send__(:send_literal, data, tag)
|
170
153
|
end
|
154
|
+
end
|
171
155
|
|
172
|
-
|
156
|
+
class PartialRange < CommandData # :nodoc:
|
157
|
+
uint32_max = 2**32 - 1
|
158
|
+
POS_RANGE = 1..uint32_max
|
159
|
+
NEG_RANGE = -uint32_max..-1
|
160
|
+
Positive = ->{ (_1 in Range) and POS_RANGE.cover?(_1) }
|
161
|
+
Negative = ->{ (_1 in Range) and NEG_RANGE.cover?(_1) }
|
162
|
+
|
163
|
+
def initialize(data:)
|
164
|
+
min, max = case data
|
165
|
+
in Range
|
166
|
+
data.minmax.map { Integer _1 }
|
167
|
+
in ResponseParser::Patterns::PARTIAL_RANGE
|
168
|
+
data.split(":").map { Integer _1 }.minmax
|
169
|
+
else
|
170
|
+
raise ArgumentError, "invalid partial range input: %p" % [data]
|
171
|
+
end
|
172
|
+
data = min..max
|
173
|
+
unless data in Positive | Negative
|
174
|
+
raise ArgumentError, "invalid partial-range: %p" % [data]
|
175
|
+
end
|
176
|
+
super
|
177
|
+
rescue TypeError, RangeError
|
178
|
+
raise ArgumentError, "expected range min/max to be Integers"
|
173
179
|
end
|
174
180
|
|
175
|
-
|
181
|
+
def formatted = "%d:%d" % data.minmax
|
176
182
|
|
177
|
-
def
|
178
|
-
|
183
|
+
def send_data(imap, tag)
|
184
|
+
imap.__send__(:put_string, formatted)
|
179
185
|
end
|
180
186
|
end
|
181
187
|
|
182
188
|
# *DEPRECATED*. Replaced by SequenceSet.
|
183
|
-
class MessageSet # :nodoc:
|
189
|
+
class MessageSet < CommandData # :nodoc:
|
184
190
|
def send_data(imap, tag)
|
185
|
-
imap.__send__(:put_string, format_internal(
|
191
|
+
imap.__send__(:put_string, format_internal(data))
|
186
192
|
end
|
187
193
|
|
188
194
|
def validate
|
189
|
-
validate_internal(
|
195
|
+
validate_internal(data)
|
190
196
|
end
|
191
197
|
|
192
198
|
private
|
193
199
|
|
194
|
-
def initialize(data)
|
195
|
-
|
200
|
+
def initialize(data:)
|
201
|
+
super
|
196
202
|
warn("DEPRECATED: #{MessageSet} should be replaced with #{SequenceSet}.",
|
197
203
|
uplevel: 1, category: :deprecated)
|
198
204
|
begin
|
@@ -246,22 +252,18 @@ module Net
|
|
246
252
|
end
|
247
253
|
end
|
248
254
|
|
249
|
-
class ClientID # :nodoc:
|
255
|
+
class ClientID < CommandData # :nodoc:
|
250
256
|
|
251
257
|
def send_data(imap, tag)
|
252
|
-
imap.__send__(:send_data, format_internal(
|
258
|
+
imap.__send__(:send_data, format_internal(data), tag)
|
253
259
|
end
|
254
260
|
|
255
261
|
def validate
|
256
|
-
validate_internal(
|
262
|
+
validate_internal(data)
|
257
263
|
end
|
258
264
|
|
259
265
|
private
|
260
266
|
|
261
|
-
def initialize(data)
|
262
|
-
@data = data
|
263
|
-
end
|
264
|
-
|
265
267
|
def validate_internal(client_id)
|
266
268
|
client_id.to_h.each do |k,v|
|
267
269
|
unless StringFormatter.valid_string?(k)
|
data/lib/net/imap/config.rb
CHANGED
@@ -75,7 +75,7 @@ module Net
|
|
75
75
|
#
|
76
76
|
# client = Net::IMAP.new(hostname, config: :future)
|
77
77
|
# client.config.sasl_ir # => true
|
78
|
-
# client.config.responses_without_block # => :
|
78
|
+
# client.config.responses_without_block # => :frozen_dup
|
79
79
|
#
|
80
80
|
# The versioned default configs inherit certain specific config options from
|
81
81
|
# Config.global, for example #debug:
|
@@ -109,9 +109,11 @@ module Net
|
|
109
109
|
# [+:future+]
|
110
110
|
# The _planned_ eventual config for some future +x.y+ version.
|
111
111
|
#
|
112
|
-
# For example, to
|
112
|
+
# For example, to disable all currently deprecated behavior:
|
113
113
|
# client = Net::IMAP.new(hostname, config: :future)
|
114
|
-
# client.
|
114
|
+
# client.config.response_without_args # => :frozen_dup
|
115
|
+
# client.responses.frozen? # => true
|
116
|
+
# client.responses.values.all?(&:frozen?) # => true
|
115
117
|
#
|
116
118
|
# == Thread Safety
|
117
119
|
#
|
@@ -285,6 +287,67 @@ module Net
|
|
285
287
|
#
|
286
288
|
# Alias for responses_without_block
|
287
289
|
|
290
|
+
# Whether ResponseParser should use the deprecated UIDPlusData or
|
291
|
+
# CopyUIDData for +COPYUID+ response codes, and UIDPlusData or
|
292
|
+
# AppendUIDData for +APPENDUID+ response codes.
|
293
|
+
#
|
294
|
+
# UIDPlusData stores its data in arrays of numbers, which is vulnerable to
|
295
|
+
# a memory exhaustion denial of service attack from an untrusted or
|
296
|
+
# compromised server. Set this option to +false+ to completely block this
|
297
|
+
# vulnerability. Otherwise, parser_max_deprecated_uidplus_data_size
|
298
|
+
# mitigates this vulnerability.
|
299
|
+
#
|
300
|
+
# AppendUIDData and CopyUIDData are _mostly_ backward-compatible with
|
301
|
+
# UIDPlusData. Most applications should be able to upgrade with little
|
302
|
+
# or no changes.
|
303
|
+
#
|
304
|
+
# <em>(Parser support for +UIDPLUS+ added in +v0.3.2+.)</em>
|
305
|
+
#
|
306
|
+
# <em>(Config option added in +v0.4.19+ and +v0.5.6+.)</em>
|
307
|
+
#
|
308
|
+
# <em>UIDPlusData will be removed in +v0.6+ and this config setting will
|
309
|
+
# be ignored.</em>
|
310
|
+
#
|
311
|
+
# ==== Valid options
|
312
|
+
#
|
313
|
+
# [+true+ <em>(original default)</em>]
|
314
|
+
# ResponseParser only uses UIDPlusData.
|
315
|
+
#
|
316
|
+
# [+:up_to_max_size+ <em>(default since +v0.5.6+)</em>]
|
317
|
+
# ResponseParser uses UIDPlusData when the +uid-set+ size is below
|
318
|
+
# parser_max_deprecated_uidplus_data_size. Above that size,
|
319
|
+
# ResponseParser uses AppendUIDData or CopyUIDData.
|
320
|
+
#
|
321
|
+
# [+false+ <em>(planned default for +v0.6+)</em>]
|
322
|
+
# ResponseParser _only_ uses AppendUIDData and CopyUIDData.
|
323
|
+
attr_accessor :parser_use_deprecated_uidplus_data, type: [
|
324
|
+
true, :up_to_max_size, false
|
325
|
+
]
|
326
|
+
|
327
|
+
# The maximum +uid-set+ size that ResponseParser will parse into
|
328
|
+
# deprecated UIDPlusData. This limit only applies when
|
329
|
+
# parser_use_deprecated_uidplus_data is not +false+.
|
330
|
+
#
|
331
|
+
# <em>(Parser support for +UIDPLUS+ added in +v0.3.2+.)</em>
|
332
|
+
#
|
333
|
+
# <em>Support for limiting UIDPlusData to a maximum size was added in
|
334
|
+
# +v0.3.8+, +v0.4.19+, and +v0.5.6+.</em>
|
335
|
+
#
|
336
|
+
# <em>UIDPlusData will be removed in +v0.6+.</em>
|
337
|
+
#
|
338
|
+
# ==== Versioned Defaults
|
339
|
+
#
|
340
|
+
# Because this limit guards against a remote server causing catastrophic
|
341
|
+
# memory exhaustion, the versioned default (used by #load_defaults) also
|
342
|
+
# applies to versions without the feature.
|
343
|
+
#
|
344
|
+
# * +0.3+ and prior: <tt>10,000</tt>
|
345
|
+
# * +0.4+: <tt>1,000</tt>
|
346
|
+
# * +0.5+: <tt>100</tt>
|
347
|
+
# * +0.6+: <tt>0</tt>
|
348
|
+
#
|
349
|
+
attr_accessor :parser_max_deprecated_uidplus_data_size, type: Integer
|
350
|
+
|
288
351
|
# Creates a new config object and initialize its attribute with +attrs+.
|
289
352
|
#
|
290
353
|
# If +parent+ is not given, the global config is used by default.
|
@@ -365,6 +428,8 @@ module Net
|
|
365
428
|
sasl_ir: true,
|
366
429
|
enforce_logindisabled: true,
|
367
430
|
responses_without_block: :warn,
|
431
|
+
parser_use_deprecated_uidplus_data: :up_to_max_size,
|
432
|
+
parser_max_deprecated_uidplus_data_size: 100,
|
368
433
|
).freeze
|
369
434
|
|
370
435
|
@global = default.new
|
@@ -376,6 +441,8 @@ module Net
|
|
376
441
|
sasl_ir: false,
|
377
442
|
responses_without_block: :silence_deprecation_warning,
|
378
443
|
enforce_logindisabled: false,
|
444
|
+
parser_use_deprecated_uidplus_data: true,
|
445
|
+
parser_max_deprecated_uidplus_data_size: 10_000,
|
379
446
|
).freeze
|
380
447
|
version_defaults[0.0] = Config[0]
|
381
448
|
version_defaults[0.1] = Config[0]
|
@@ -384,12 +451,15 @@ module Net
|
|
384
451
|
|
385
452
|
version_defaults[0.4] = Config[0.3].dup.update(
|
386
453
|
sasl_ir: true,
|
454
|
+
parser_max_deprecated_uidplus_data_size: 1000,
|
387
455
|
).freeze
|
388
456
|
|
389
457
|
version_defaults[0.5] = Config[:current]
|
390
458
|
|
391
459
|
version_defaults[0.6] = Config[0.5].dup.update(
|
392
460
|
responses_without_block: :frozen_dup,
|
461
|
+
parser_use_deprecated_uidplus_data: false,
|
462
|
+
parser_max_deprecated_uidplus_data_size: 0,
|
393
463
|
).freeze
|
394
464
|
version_defaults[:next] = Config[0.6]
|
395
465
|
version_defaults[:future] = Config[:next]
|
@@ -0,0 +1,226 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Some of the code in this file was copied from the polyfill-data gem.
|
4
|
+
#
|
5
|
+
# MIT License
|
6
|
+
#
|
7
|
+
# Copyright (c) 2023 Jim Gay, Joel Drapper, Nicholas Evans
|
8
|
+
#
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
10
|
+
# of this software and associated documentation files (the "Software"), to deal
|
11
|
+
# in the Software without restriction, including without limitation the rights
|
12
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
13
|
+
# copies of the Software, and to permit persons to whom the Software is
|
14
|
+
# furnished to do so, subject to the following conditions:
|
15
|
+
#
|
16
|
+
# The above copyright notice and this permission notice shall be included in all
|
17
|
+
# copies or substantial portions of the Software.
|
18
|
+
#
|
19
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
20
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
21
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
22
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
23
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
24
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
25
|
+
# SOFTWARE.
|
26
|
+
|
27
|
+
|
28
|
+
module Net
|
29
|
+
class IMAP
|
30
|
+
data_or_object = RUBY_VERSION >= "3.2.0" ? ::Data : Object
|
31
|
+
class DataLite < data_or_object
|
32
|
+
def encode_with(coder) coder.map = to_h.transform_keys(&:to_s) end
|
33
|
+
def init_with(coder) initialize(**coder.map.transform_keys(&:to_sym)) end
|
34
|
+
end
|
35
|
+
|
36
|
+
Data = DataLite
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# :nocov:
|
41
|
+
# Need to skip test coverage for the rest, because it isn't loaded by ruby 3.2+.
|
42
|
+
return if RUBY_VERSION >= "3.2.0"
|
43
|
+
|
44
|
+
module Net
|
45
|
+
class IMAP
|
46
|
+
# DataLite is a temporary substitute for ruby 3.2's +Data+ class. DataLite
|
47
|
+
# is aliased as Net::IMAP::Data, so that code using it won't need to be
|
48
|
+
# updated when it is removed.
|
49
|
+
#
|
50
|
+
# See {ruby 3.2's documentation for Data}[https://docs.ruby-lang.org/en/3.2/Data.html].
|
51
|
+
#
|
52
|
+
# [When running ruby 3.1]
|
53
|
+
# This class reimplements the API for ruby 3.2's +Data+, and should be
|
54
|
+
# compatible for nearly all use-cases. This reimplementation <em>will be
|
55
|
+
# removed</em> in +net-imap+ 0.6, when support for ruby 3.1 is dropped.
|
56
|
+
#
|
57
|
+
# _NOTE:_ +net-imap+ no longer supports ruby versions prior to 3.1.
|
58
|
+
# [When running ruby >= 3.2]
|
59
|
+
# This class inherits from +Data+ and _only_ defines the methods needed
|
60
|
+
# for YAML serialization. This will be dropped when +psych+ adds support
|
61
|
+
# for +Data+.
|
62
|
+
#
|
63
|
+
# Some of the code in this class was copied or adapted from the
|
64
|
+
# {polyfill-data gem}[https://rubygems.org/gems/polyfill-data], by Jim Gay
|
65
|
+
# and Joel Drapper, under the MIT license terms.
|
66
|
+
class DataLite
|
67
|
+
singleton_class.undef_method :new
|
68
|
+
|
69
|
+
TYPE_ERROR = "%p is not a symbol nor a string"
|
70
|
+
ATTRSET_ERROR = "invalid data member: %p"
|
71
|
+
DUP_ERROR = "duplicate member: %p"
|
72
|
+
ARITY_ERROR = "wrong number of arguments (given %d, expected %s)"
|
73
|
+
private_constant :TYPE_ERROR, :ATTRSET_ERROR, :DUP_ERROR, :ARITY_ERROR
|
74
|
+
|
75
|
+
# Defines a new Data class.
|
76
|
+
#
|
77
|
+
# _NOTE:_ Unlike ruby 3.2's +Data.define+, DataLite.define only supports
|
78
|
+
# member names which are valid local variable names. Member names can't
|
79
|
+
# be keywords (e.g: +next+ or +class+) or start with capital letters, "@",
|
80
|
+
# etc.
|
81
|
+
def self.define(*args, &block)
|
82
|
+
members = args.each_with_object({}) do |arg, members|
|
83
|
+
arg = arg.to_str unless arg in Symbol | String if arg.respond_to?(:to_str)
|
84
|
+
arg = arg.to_sym if arg in String
|
85
|
+
arg in Symbol or raise TypeError, TYPE_ERROR % [arg]
|
86
|
+
arg in %r{=} and raise ArgumentError, ATTRSET_ERROR % [arg]
|
87
|
+
members.key?(arg) and raise ArgumentError, DUP_ERROR % [arg]
|
88
|
+
members[arg] = true
|
89
|
+
end
|
90
|
+
members = members.keys.freeze
|
91
|
+
|
92
|
+
klass = ::Class.new(self)
|
93
|
+
|
94
|
+
klass.singleton_class.undef_method :define
|
95
|
+
klass.define_singleton_method(:members) { members }
|
96
|
+
|
97
|
+
def klass.new(*args, **kwargs, &block)
|
98
|
+
if kwargs.size.positive?
|
99
|
+
if args.size.positive?
|
100
|
+
raise ArgumentError, ARITY_ERROR % [args.size, 0]
|
101
|
+
end
|
102
|
+
elsif members.size < args.size
|
103
|
+
expected = members.size.zero? ? 0 : 0..members.size
|
104
|
+
raise ArgumentError, ARITY_ERROR % [args.size, expected]
|
105
|
+
else
|
106
|
+
kwargs = Hash[members.take(args.size).zip(args)]
|
107
|
+
end
|
108
|
+
allocate.tap do |instance|
|
109
|
+
instance.__send__(:initialize, **kwargs, &block)
|
110
|
+
end.freeze
|
111
|
+
end
|
112
|
+
|
113
|
+
klass.singleton_class.alias_method :[], :new
|
114
|
+
klass.attr_reader(*members)
|
115
|
+
|
116
|
+
# Dynamically defined initializer methods are in an included module,
|
117
|
+
# rather than directly on DataLite (like in ruby 3.2+):
|
118
|
+
# * simpler to handle required kwarg ArgumentErrors
|
119
|
+
# * easier to ensure consistent ivar assignment order (object shape)
|
120
|
+
# * faster than instance_variable_set
|
121
|
+
klass.include(Module.new do
|
122
|
+
if members.any?
|
123
|
+
kwargs = members.map{"#{_1.name}:"}.join(", ")
|
124
|
+
params = members.map(&:name).join(", ")
|
125
|
+
ivars = members.map{"@#{_1.name}"}.join(", ")
|
126
|
+
attrs = members.map{"attrs[:#{_1.name}]"}.join(", ")
|
127
|
+
module_eval <<~RUBY, __FILE__, __LINE__ + 1
|
128
|
+
protected
|
129
|
+
def initialize(#{kwargs}) #{ivars} = #{params}; freeze end
|
130
|
+
def marshal_load(attrs) #{ivars} = #{attrs}; freeze end
|
131
|
+
RUBY
|
132
|
+
end
|
133
|
+
end)
|
134
|
+
|
135
|
+
klass.module_eval do _1.module_eval(&block) end if block_given?
|
136
|
+
|
137
|
+
klass
|
138
|
+
end
|
139
|
+
|
140
|
+
##
|
141
|
+
# singleton-method: new
|
142
|
+
# call-seq:
|
143
|
+
# new(*args) -> instance
|
144
|
+
# new(**kwargs) -> instance
|
145
|
+
#
|
146
|
+
# Constuctor for classes defined with ::define.
|
147
|
+
#
|
148
|
+
# Aliased as ::[].
|
149
|
+
|
150
|
+
##
|
151
|
+
# singleton-method: []
|
152
|
+
# call-seq:
|
153
|
+
# ::[](*args) -> instance
|
154
|
+
# ::[](**kwargs) -> instance
|
155
|
+
#
|
156
|
+
# Constuctor for classes defined with ::define.
|
157
|
+
#
|
158
|
+
# Alias for ::new
|
159
|
+
|
160
|
+
##
|
161
|
+
def members; self.class.members end
|
162
|
+
def to_h(&block) block ? __to_h__.to_h(&block) : __to_h__ end
|
163
|
+
def hash; [self.class, __to_h__].hash end
|
164
|
+
def ==(other) self.class == other.class && to_h == other.to_h end
|
165
|
+
def eql?(other) self.class == other.class && hash == other.hash end
|
166
|
+
def deconstruct; __to_h__.values end
|
167
|
+
|
168
|
+
def deconstruct_keys(keys)
|
169
|
+
raise TypeError unless keys.is_a?(Array) || keys.nil?
|
170
|
+
return __to_h__ if keys&.first.nil?
|
171
|
+
__to_h__.slice(*keys)
|
172
|
+
end
|
173
|
+
|
174
|
+
def with(**kwargs)
|
175
|
+
return self if kwargs.empty?
|
176
|
+
self.class.new(**__to_h__.merge(kwargs))
|
177
|
+
end
|
178
|
+
|
179
|
+
def inspect
|
180
|
+
__inspect_guard__(self) do |seen|
|
181
|
+
return "#<data #{self.class}:...>" if seen
|
182
|
+
attrs = __to_h__.map {|kv| "%s=%p" % kv }.join(", ")
|
183
|
+
display = ["data", self.class.name, attrs].compact.join(" ")
|
184
|
+
"#<#{display}>"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
alias_method :to_s, :inspect
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
def initialize_copy(source) super.freeze end
|
192
|
+
def marshal_dump; __to_h__ end
|
193
|
+
|
194
|
+
def __to_h__; Hash[members.map {|m| [m, send(m)] }] end
|
195
|
+
|
196
|
+
# Yields +true+ if +obj+ has been seen already, +false+ if it hasn't.
|
197
|
+
# Marks +obj+ as seen inside the block, so circuler references don't
|
198
|
+
# recursively trigger a SystemStackError (stack level too deep).
|
199
|
+
#
|
200
|
+
# Making circular references inside a Data object _should_ be very
|
201
|
+
# uncommon, but we'll support them for the sake of completeness.
|
202
|
+
def __inspect_guard__(obj)
|
203
|
+
preexisting = Thread.current[:__net_imap_data__inspect__]
|
204
|
+
Thread.current[:__net_imap_data__inspect__] ||= {}.compare_by_identity
|
205
|
+
inspect_guard = Thread.current[:__net_imap_data__inspect__]
|
206
|
+
if inspect_guard.include?(obj)
|
207
|
+
yield true
|
208
|
+
else
|
209
|
+
begin
|
210
|
+
inspect_guard[obj] = true
|
211
|
+
yield false
|
212
|
+
ensure
|
213
|
+
inspect_guard.delete(obj)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
ensure
|
217
|
+
unless preexisting.equal?(inspect_guard)
|
218
|
+
Thread.current[:__net_imap_data__inspect__] = preexisting
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
end
|
223
|
+
|
224
|
+
end
|
225
|
+
end
|
226
|
+
# :nocov:
|