net-imap 0.4.18 → 0.4.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/net/imap/config/attr_type_coercion.rb +25 -21
- data/lib/net/imap/config.rb +169 -19
- data/lib/net/imap/errors.rb +33 -0
- data/lib/net/imap/response_data.rb +3 -54
- data/lib/net/imap/response_parser.rb +28 -13
- data/lib/net/imap/response_reader.rb +75 -0
- data/lib/net/imap/sequence_set.rb +254 -117
- data/lib/net/imap/uidplus_data.rb +326 -0
- data/lib/net/imap.rb +114 -41
- metadata +5 -6
@@ -0,0 +1,326 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net
|
4
|
+
class IMAP < Protocol
|
5
|
+
|
6
|
+
# *NOTE:* <em>UIDPlusData is deprecated and will be removed in the +0.6.0+
|
7
|
+
# release.</em> To use AppendUIDData and CopyUIDData before +0.6.0+, set
|
8
|
+
# Config#parser_use_deprecated_uidplus_data to +false+.
|
9
|
+
#
|
10
|
+
# UIDPlusData represents the ResponseCode#data that accompanies the
|
11
|
+
# +APPENDUID+ and +COPYUID+ {response codes}[rdoc-ref:ResponseCode].
|
12
|
+
#
|
13
|
+
# A server that supports +UIDPLUS+ should send UIDPlusData in response to
|
14
|
+
# the append[rdoc-ref:Net::IMAP#append], copy[rdoc-ref:Net::IMAP#copy],
|
15
|
+
# move[rdoc-ref:Net::IMAP#move], {uid copy}[rdoc-ref:Net::IMAP#uid_copy],
|
16
|
+
# and {uid move}[rdoc-ref:Net::IMAP#uid_move] commands---unless the
|
17
|
+
# destination mailbox reports +UIDNOTSTICKY+.
|
18
|
+
#
|
19
|
+
# Note that append[rdoc-ref:Net::IMAP#append], copy[rdoc-ref:Net::IMAP#copy]
|
20
|
+
# and {uid_copy}[rdoc-ref:Net::IMAP#uid_copy] return UIDPlusData in their
|
21
|
+
# TaggedResponse. But move[rdoc-ref:Net::IMAP#copy] and
|
22
|
+
# {uid_move}[rdoc-ref:Net::IMAP#uid_move] _should_ send UIDPlusData in an
|
23
|
+
# UntaggedResponse response before sending their TaggedResponse. However
|
24
|
+
# some servers do send UIDPlusData in the TaggedResponse for +MOVE+
|
25
|
+
# commands---this complies with the older +UIDPLUS+ specification but is
|
26
|
+
# discouraged by the +MOVE+ extension and disallowed by +IMAP4rev2+.
|
27
|
+
#
|
28
|
+
# == Required capability
|
29
|
+
# Requires either +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315]]
|
30
|
+
# or +IMAP4rev2+ capability.
|
31
|
+
#
|
32
|
+
class UIDPlusData < Struct.new(:uidvalidity, :source_uids, :assigned_uids)
|
33
|
+
##
|
34
|
+
# method: uidvalidity
|
35
|
+
# :call-seq: uidvalidity -> nonzero uint32
|
36
|
+
#
|
37
|
+
# The UIDVALIDITY of the destination mailbox.
|
38
|
+
|
39
|
+
##
|
40
|
+
# method: source_uids
|
41
|
+
# :call-seq: source_uids -> nil or an array of nonzero uint32
|
42
|
+
#
|
43
|
+
# The UIDs of the copied or moved messages.
|
44
|
+
#
|
45
|
+
# Note:: Returns +nil+ for Net::IMAP#append.
|
46
|
+
|
47
|
+
##
|
48
|
+
# method: assigned_uids
|
49
|
+
# :call-seq: assigned_uids -> an array of nonzero uint32
|
50
|
+
#
|
51
|
+
# The newly assigned UIDs of the copied, moved, or appended messages.
|
52
|
+
#
|
53
|
+
# Note:: This always returns an array, even when it contains only one UID.
|
54
|
+
|
55
|
+
##
|
56
|
+
# :call-seq: uid_mapping -> nil or a hash
|
57
|
+
#
|
58
|
+
# Returns a hash mapping each source UID to the newly assigned destination
|
59
|
+
# UID.
|
60
|
+
#
|
61
|
+
# Note:: Returns +nil+ for Net::IMAP#append.
|
62
|
+
def uid_mapping
|
63
|
+
source_uids&.zip(assigned_uids)&.to_h
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# This replaces the `Data.define` polyfill that's used by net-imap 0.5.
|
68
|
+
class Data_define__uidvalidity___assigned_uids_ # :no-doc:
|
69
|
+
attr_reader :uidvalidity, :assigned_uids
|
70
|
+
|
71
|
+
def self.[](...) new(...) end
|
72
|
+
def self.new(uidvalidity = (args = false; nil),
|
73
|
+
assigned_uids = nil,
|
74
|
+
**kwargs)
|
75
|
+
if kwargs.empty?
|
76
|
+
super(uidvalidity: uidvalidity, assigned_uids: assigned_uids)
|
77
|
+
elsif !args
|
78
|
+
super
|
79
|
+
else
|
80
|
+
raise ArgumentError, "sent both positional and keyword args"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def ==(other)
|
85
|
+
self.class == other.class &&
|
86
|
+
self.uidvalidity == other.uidvalidity &&
|
87
|
+
self.assigned_uids == other.assigned_uids
|
88
|
+
end
|
89
|
+
|
90
|
+
def eql?(other)
|
91
|
+
self.class.eql?(other.class) &&
|
92
|
+
self.uidvalidity.eql?(other.uidvalidity) &&
|
93
|
+
self.assigned_uids.eql?(other.assigned_uids)
|
94
|
+
end
|
95
|
+
|
96
|
+
def hash; [self.class, uidvalidity, assigned_uids].hash end
|
97
|
+
|
98
|
+
def initialize(uidvalidity:, assigned_uids:)
|
99
|
+
@uidvalidity = uidvalidity
|
100
|
+
@assigned_uids = assigned_uids
|
101
|
+
freeze
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# >>>
|
106
|
+
# *NOTE:* <em>AppendUIDData will replace UIDPlusData for +APPENDUID+ in the
|
107
|
+
# +0.6.0+ release.</em> To use AppendUIDData before +0.6.0+, set
|
108
|
+
# Config#parser_use_deprecated_uidplus_data to +false+.
|
109
|
+
#
|
110
|
+
# AppendUIDData represents the ResponseCode#data that accompanies the
|
111
|
+
# +APPENDUID+ {response code}[rdoc-ref:ResponseCode].
|
112
|
+
#
|
113
|
+
# A server that supports +UIDPLUS+ (or +IMAP4rev2+) should send
|
114
|
+
# AppendUIDData inside every TaggedResponse returned by the
|
115
|
+
# append[rdoc-ref:Net::IMAP#append] command---unless the target mailbox
|
116
|
+
# reports +UIDNOTSTICKY+.
|
117
|
+
#
|
118
|
+
# == Required capability
|
119
|
+
# Requires either +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315]]
|
120
|
+
# or +IMAP4rev2+ capability.
|
121
|
+
class AppendUIDData < Data_define__uidvalidity___assigned_uids_
|
122
|
+
def initialize(uidvalidity:, assigned_uids:)
|
123
|
+
uidvalidity = Integer(uidvalidity)
|
124
|
+
assigned_uids = SequenceSet[assigned_uids]
|
125
|
+
NumValidator.ensure_nz_number(uidvalidity)
|
126
|
+
if assigned_uids.include_star?
|
127
|
+
raise DataFormatError, "uid-set cannot contain '*'"
|
128
|
+
end
|
129
|
+
super
|
130
|
+
end
|
131
|
+
|
132
|
+
##
|
133
|
+
# attr_reader: uidvalidity
|
134
|
+
# :call-seq: uidvalidity -> nonzero uint32
|
135
|
+
#
|
136
|
+
# The UIDVALIDITY of the destination mailbox.
|
137
|
+
|
138
|
+
##
|
139
|
+
# attr_reader: assigned_uids
|
140
|
+
#
|
141
|
+
# A SequenceSet with the newly assigned UIDs of the appended messages.
|
142
|
+
|
143
|
+
# Returns the number of messages that have been appended.
|
144
|
+
def size
|
145
|
+
assigned_uids.count_with_duplicates
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# This replaces the `Data.define` polyfill that's used by net-imap 0.5.
|
150
|
+
class Data_define__uidvalidity___source_uids___assigned_uids_ # :no-doc:
|
151
|
+
attr_reader :uidvalidity, :source_uids, :assigned_uids
|
152
|
+
|
153
|
+
def self.[](...) new(...) end
|
154
|
+
def self.new(uidvalidity = (args = false; nil),
|
155
|
+
source_uids = nil,
|
156
|
+
assigned_uids = nil,
|
157
|
+
**kwargs)
|
158
|
+
if kwargs.empty?
|
159
|
+
super(uidvalidity: uidvalidity,
|
160
|
+
source_uids: source_uids,
|
161
|
+
assigned_uids: assigned_uids)
|
162
|
+
elsif !args
|
163
|
+
super(**kwargs)
|
164
|
+
else
|
165
|
+
raise ArgumentError, "sent both positional and keyword args"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def initialize(uidvalidity:, source_uids:, assigned_uids:)
|
170
|
+
@uidvalidity = uidvalidity
|
171
|
+
@source_uids = source_uids
|
172
|
+
@assigned_uids = assigned_uids
|
173
|
+
freeze
|
174
|
+
end
|
175
|
+
|
176
|
+
def ==(other)
|
177
|
+
self.class == other.class &&
|
178
|
+
self.uidvalidity == other.uidvalidity &&
|
179
|
+
self.source_uids == other.source_uids
|
180
|
+
self.assigned_uids == other.assigned_uids
|
181
|
+
end
|
182
|
+
|
183
|
+
def eql?(other)
|
184
|
+
self.class.eql?(other.class) &&
|
185
|
+
self.uidvalidity.eql?(other.uidvalidity) &&
|
186
|
+
self.source_uids.eql?(other.source_uids)
|
187
|
+
self.assigned_uids.eql?(other.assigned_uids)
|
188
|
+
end
|
189
|
+
|
190
|
+
def hash; [self.class, uidvalidity, source_uids, assigned_uids].hash end
|
191
|
+
end
|
192
|
+
|
193
|
+
# >>>
|
194
|
+
# *NOTE:* <em>CopyUIDData will replace UIDPlusData for +COPYUID+ in the
|
195
|
+
# +0.6.0+ release.</em> To use CopyUIDData before +0.6.0+, set
|
196
|
+
# Config#parser_use_deprecated_uidplus_data to +false+.
|
197
|
+
#
|
198
|
+
# CopyUIDData represents the ResponseCode#data that accompanies the
|
199
|
+
# +COPYUID+ {response code}[rdoc-ref:ResponseCode].
|
200
|
+
#
|
201
|
+
# A server that supports +UIDPLUS+ (or +IMAP4rev2+) should send CopyUIDData
|
202
|
+
# in response to
|
203
|
+
# copy[rdoc-ref:Net::IMAP#copy], {uid_copy}[rdoc-ref:Net::IMAP#uid_copy],
|
204
|
+
# move[rdoc-ref:Net::IMAP#copy], and {uid_move}[rdoc-ref:Net::IMAP#uid_move]
|
205
|
+
# commands---unless the destination mailbox reports +UIDNOTSTICKY+.
|
206
|
+
#
|
207
|
+
# Note that copy[rdoc-ref:Net::IMAP#copy] and
|
208
|
+
# {uid_copy}[rdoc-ref:Net::IMAP#uid_copy] return CopyUIDData in their
|
209
|
+
# TaggedResponse. But move[rdoc-ref:Net::IMAP#copy] and
|
210
|
+
# {uid_move}[rdoc-ref:Net::IMAP#uid_move] _should_ send CopyUIDData in an
|
211
|
+
# UntaggedResponse response before sending their TaggedResponse. However
|
212
|
+
# some servers do send CopyUIDData in the TaggedResponse for +MOVE+
|
213
|
+
# commands---this complies with the older +UIDPLUS+ specification but is
|
214
|
+
# discouraged by the +MOVE+ extension and disallowed by +IMAP4rev2+.
|
215
|
+
#
|
216
|
+
# == Required capability
|
217
|
+
# Requires either +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315]]
|
218
|
+
# or +IMAP4rev2+ capability.
|
219
|
+
class CopyUIDData < Data_define__uidvalidity___source_uids___assigned_uids_
|
220
|
+
def initialize(uidvalidity:, source_uids:, assigned_uids:)
|
221
|
+
uidvalidity = Integer(uidvalidity)
|
222
|
+
source_uids = SequenceSet[source_uids]
|
223
|
+
assigned_uids = SequenceSet[assigned_uids]
|
224
|
+
NumValidator.ensure_nz_number(uidvalidity)
|
225
|
+
if source_uids.include_star? || assigned_uids.include_star?
|
226
|
+
raise DataFormatError, "uid-set cannot contain '*'"
|
227
|
+
elsif source_uids.count_with_duplicates != assigned_uids.count_with_duplicates
|
228
|
+
raise DataFormatError, "mismatched uid-set sizes for %s and %s" % [
|
229
|
+
source_uids, assigned_uids
|
230
|
+
]
|
231
|
+
end
|
232
|
+
super
|
233
|
+
end
|
234
|
+
|
235
|
+
##
|
236
|
+
# attr_reader: uidvalidity
|
237
|
+
#
|
238
|
+
# The +UIDVALIDITY+ of the destination mailbox (a nonzero unsigned 32 bit
|
239
|
+
# integer).
|
240
|
+
|
241
|
+
##
|
242
|
+
# attr_reader: source_uids
|
243
|
+
#
|
244
|
+
# A SequenceSet with the original UIDs of the copied or moved messages.
|
245
|
+
|
246
|
+
##
|
247
|
+
# attr_reader: assigned_uids
|
248
|
+
#
|
249
|
+
# A SequenceSet with the newly assigned UIDs of the copied or moved
|
250
|
+
# messages.
|
251
|
+
|
252
|
+
# Returns the number of messages that have been copied or moved.
|
253
|
+
# source_uids and the assigned_uids will both the same number of UIDs.
|
254
|
+
def size
|
255
|
+
assigned_uids.count_with_duplicates
|
256
|
+
end
|
257
|
+
|
258
|
+
# :call-seq:
|
259
|
+
# assigned_uid_for(source_uid) -> uid
|
260
|
+
# self[source_uid] -> uid
|
261
|
+
#
|
262
|
+
# Returns the UID in the destination mailbox for the message that was
|
263
|
+
# copied from +source_uid+ in the source mailbox.
|
264
|
+
#
|
265
|
+
# This is the reverse of #source_uid_for.
|
266
|
+
#
|
267
|
+
# Related: source_uid_for, each_uid_pair, uid_mapping
|
268
|
+
def assigned_uid_for(source_uid)
|
269
|
+
idx = source_uids.find_ordered_index(source_uid) and
|
270
|
+
assigned_uids.ordered_at(idx)
|
271
|
+
end
|
272
|
+
alias :[] :assigned_uid_for
|
273
|
+
|
274
|
+
# :call-seq:
|
275
|
+
# source_uid_for(assigned_uid) -> uid
|
276
|
+
#
|
277
|
+
# Returns the UID in the source mailbox for the message that was copied to
|
278
|
+
# +assigned_uid+ in the source mailbox.
|
279
|
+
#
|
280
|
+
# This is the reverse of #assigned_uid_for.
|
281
|
+
#
|
282
|
+
# Related: assigned_uid_for, each_uid_pair, uid_mapping
|
283
|
+
def source_uid_for(assigned_uid)
|
284
|
+
idx = assigned_uids.find_ordered_index(assigned_uid) and
|
285
|
+
source_uids.ordered_at(idx)
|
286
|
+
end
|
287
|
+
|
288
|
+
# Yields a pair of UIDs for each copied message. The first is the
|
289
|
+
# message's UID in the source mailbox and the second is the UID in the
|
290
|
+
# destination mailbox.
|
291
|
+
#
|
292
|
+
# Returns an enumerator when no block is given.
|
293
|
+
#
|
294
|
+
# Please note the warning on uid_mapping before calling methods like
|
295
|
+
# +to_h+ or +to_a+ on the returned enumerator.
|
296
|
+
#
|
297
|
+
# Related: uid_mapping, assigned_uid_for, source_uid_for
|
298
|
+
def each_uid_pair
|
299
|
+
return enum_for(__method__) unless block_given?
|
300
|
+
source_uids.each_ordered_number.lazy
|
301
|
+
.zip(assigned_uids.each_ordered_number.lazy) do
|
302
|
+
|source_uid, assigned_uid|
|
303
|
+
yield source_uid, assigned_uid
|
304
|
+
end
|
305
|
+
end
|
306
|
+
alias each_pair each_uid_pair
|
307
|
+
alias each each_uid_pair
|
308
|
+
|
309
|
+
# :call-seq: uid_mapping -> hash
|
310
|
+
#
|
311
|
+
# Returns a hash mapping each source UID to the newly assigned destination
|
312
|
+
# UID.
|
313
|
+
#
|
314
|
+
# <em>*Warning:*</em> The hash that is created may consume _much_ more
|
315
|
+
# memory than the data used to create it. When handling responses from an
|
316
|
+
# untrusted server, check #size before calling this method.
|
317
|
+
#
|
318
|
+
# Related: each_uid_pair, assigned_uid_for, source_uid_for
|
319
|
+
def uid_mapping
|
320
|
+
each_uid_pair.to_h
|
321
|
+
end
|
322
|
+
|
323
|
+
end
|
324
|
+
|
325
|
+
end
|
326
|
+
end
|
data/lib/net/imap.rb
CHANGED
@@ -43,10 +43,16 @@ module Net
|
|
43
43
|
# To work on the messages within a mailbox, the client must
|
44
44
|
# first select that mailbox, using either #select or #examine
|
45
45
|
# (for read-only access). Once the client has successfully
|
46
|
-
# selected a mailbox, they enter the
|
46
|
+
# selected a mailbox, they enter the +selected+ state, and that
|
47
47
|
# mailbox becomes the _current_ mailbox, on which mail-item
|
48
48
|
# related commands implicitly operate.
|
49
49
|
#
|
50
|
+
# === Connection state
|
51
|
+
#
|
52
|
+
# Once an IMAP connection is established, the connection is in one of four
|
53
|
+
# states: <tt>not authenticated</tt>, +authenticated+, +selected+, and
|
54
|
+
# +logout+. Most commands are valid only in certain states.
|
55
|
+
#
|
50
56
|
# === Sequence numbers and UIDs
|
51
57
|
#
|
52
58
|
# Messages have two sorts of identifiers: message sequence
|
@@ -199,6 +205,42 @@ module Net
|
|
199
205
|
#
|
200
206
|
# This script invokes the FETCH command and the SEARCH command concurrently.
|
201
207
|
#
|
208
|
+
# When running multiple commands, care must be taken to avoid ambiguity. For
|
209
|
+
# example, SEARCH responses are ambiguous about which command they are
|
210
|
+
# responding to, so search commands should not run simultaneously, unless the
|
211
|
+
# server supports +ESEARCH+ {[RFC4731]}[https://rfc-editor.org/rfc/rfc4731] or
|
212
|
+
# IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051]. See {RFC9051
|
213
|
+
# §5.5}[https://www.rfc-editor.org/rfc/rfc9051.html#section-5.5] for
|
214
|
+
# other examples of command sequences which should not be pipelined.
|
215
|
+
#
|
216
|
+
# == Unbounded memory use
|
217
|
+
#
|
218
|
+
# Net::IMAP reads server responses in a separate receiver thread per client.
|
219
|
+
# Unhandled response data is saved to #responses, and response_handlers run
|
220
|
+
# inside the receiver thread. See the list of methods for {handling server
|
221
|
+
# responses}[rdoc-ref:Net::IMAP@Handling+server+responses], below.
|
222
|
+
#
|
223
|
+
# Because the receiver thread continuously reads and saves new responses, some
|
224
|
+
# scenarios must be careful to avoid unbounded memory use:
|
225
|
+
#
|
226
|
+
# * Commands such as #list or #fetch can have an enormous number of responses.
|
227
|
+
# * Commands such as #fetch can result in an enormous size per response.
|
228
|
+
# * Long-lived connections will gradually accumulate unsolicited server
|
229
|
+
# responses, especially +EXISTS+, +FETCH+, and +EXPUNGE+ responses.
|
230
|
+
# * A buggy or untrusted server could send inappropriate responses, which
|
231
|
+
# could be very numerous, very large, and very rapid.
|
232
|
+
#
|
233
|
+
# Use paginated or limited versions of commands whenever possible.
|
234
|
+
#
|
235
|
+
# Use Config#max_response_size to impose a limit on incoming server responses
|
236
|
+
# as they are being read. <em>This is especially important for untrusted
|
237
|
+
# servers.</em>
|
238
|
+
#
|
239
|
+
# Use #add_response_handler to handle responses after each one is received.
|
240
|
+
# Use the +response_handlers+ argument to ::new to assign response handlers
|
241
|
+
# before the receiver thread is started. Use #extract_responses,
|
242
|
+
# #clear_responses, or #responses (with a block) to prune responses.
|
243
|
+
#
|
202
244
|
# == Errors
|
203
245
|
#
|
204
246
|
# An \IMAP server can send three different types of responses to indicate
|
@@ -260,8 +302,9 @@ module Net
|
|
260
302
|
#
|
261
303
|
# - Net::IMAP.new: Creates a new \IMAP client which connects immediately and
|
262
304
|
# waits for a successful server greeting before the method returns.
|
305
|
+
# - #connection_state: Returns the connection state.
|
263
306
|
# - #starttls: Asks the server to upgrade a clear-text connection to use TLS.
|
264
|
-
# - #logout: Tells the server to end the session.
|
307
|
+
# - #logout: Tells the server to end the session. Enters the +logout+ state.
|
265
308
|
# - #disconnect: Disconnects the connection (without sending #logout first).
|
266
309
|
# - #disconnected?: True if the connection has been closed.
|
267
310
|
#
|
@@ -317,37 +360,36 @@ module Net
|
|
317
360
|
# <em>In general, #capable? should be used rather than explicitly sending a
|
318
361
|
# +CAPABILITY+ command to the server.</em>
|
319
362
|
# - #noop: Allows the server to send unsolicited untagged #responses.
|
320
|
-
# - #logout: Tells the server to end the session. Enters the
|
363
|
+
# - #logout: Tells the server to end the session. Enters the +logout+ state.
|
321
364
|
#
|
322
365
|
# ==== Not Authenticated state
|
323
366
|
#
|
324
367
|
# In addition to the commands for any state, the following commands are valid
|
325
|
-
# in the
|
368
|
+
# in the +not_authenticated+ state:
|
326
369
|
#
|
327
370
|
# - #starttls: Upgrades a clear-text connection to use TLS.
|
328
371
|
#
|
329
372
|
# <em>Requires the +STARTTLS+ capability.</em>
|
330
373
|
# - #authenticate: Identifies the client to the server using the given
|
331
374
|
# {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
|
332
|
-
# and credentials. Enters the
|
375
|
+
# and credentials. Enters the +authenticated+ state.
|
333
376
|
#
|
334
377
|
# <em>The server should list <tt>"AUTH=#{mechanism}"</tt> capabilities for
|
335
378
|
# supported mechanisms.</em>
|
336
379
|
# - #login: Identifies the client to the server using a plain text password.
|
337
|
-
# Using #authenticate is
|
338
|
-
# state.
|
380
|
+
# Using #authenticate is preferred. Enters the +authenticated+ state.
|
339
381
|
#
|
340
382
|
# <em>The +LOGINDISABLED+ capability</em> <b>must NOT</b> <em>be listed.</em>
|
341
383
|
#
|
342
384
|
# ==== Authenticated state
|
343
385
|
#
|
344
386
|
# In addition to the commands for any state, the following commands are valid
|
345
|
-
# in the
|
387
|
+
# in the +authenticated+ state:
|
346
388
|
#
|
347
389
|
# - #enable: Enables backwards incompatible server extensions.
|
348
390
|
# <em>Requires the +ENABLE+ or +IMAP4rev2+ capability.</em>
|
349
|
-
# - #select: Open a mailbox and enter the
|
350
|
-
# - #examine: Open a mailbox read-only, and enter the
|
391
|
+
# - #select: Open a mailbox and enter the +selected+ state.
|
392
|
+
# - #examine: Open a mailbox read-only, and enter the +selected+ state.
|
351
393
|
# - #create: Creates a new mailbox.
|
352
394
|
# - #delete: Permanently remove a mailbox.
|
353
395
|
# - #rename: Change the name of a mailbox.
|
@@ -369,12 +411,12 @@ module Net
|
|
369
411
|
#
|
370
412
|
# ==== Selected state
|
371
413
|
#
|
372
|
-
# In addition to the commands for any state and the
|
373
|
-
# commands, the following commands are valid in the
|
414
|
+
# In addition to the commands for any state and the +authenticated+
|
415
|
+
# commands, the following commands are valid in the +selected+ state:
|
374
416
|
#
|
375
|
-
# - #close: Closes the mailbox and returns to the
|
417
|
+
# - #close: Closes the mailbox and returns to the +authenticated+ state,
|
376
418
|
# expunging deleted messages, unless the mailbox was opened as read-only.
|
377
|
-
# - #unselect: Closes the mailbox and returns to the
|
419
|
+
# - #unselect: Closes the mailbox and returns to the +authenticated+ state,
|
378
420
|
# without expunging any messages.
|
379
421
|
# <em>Requires the +UNSELECT+ or +IMAP4rev2+ capability.</em>
|
380
422
|
# - #expunge: Permanently removes messages which have the Deleted flag set.
|
@@ -395,7 +437,7 @@ module Net
|
|
395
437
|
#
|
396
438
|
# ==== Logout state
|
397
439
|
#
|
398
|
-
# No \IMAP commands are valid in the
|
440
|
+
# No \IMAP commands are valid in the +logout+ state. If the socket is still
|
399
441
|
# open, Net::IMAP will close it after receiving server confirmation.
|
400
442
|
# Exceptions will be raised by \IMAP commands that have already started and
|
401
443
|
# are waiting for a response, as well as any that are called after logout.
|
@@ -449,7 +491,7 @@ module Net
|
|
449
491
|
# ==== RFC3691: +UNSELECT+
|
450
492
|
# Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051] and also included
|
451
493
|
# above with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands].
|
452
|
-
# - #unselect: Closes the mailbox and returns to the
|
494
|
+
# - #unselect: Closes the mailbox and returns to the +authenticated+ state,
|
453
495
|
# without expunging any messages.
|
454
496
|
#
|
455
497
|
# ==== RFC4314: +ACL+
|
@@ -719,7 +761,7 @@ module Net
|
|
719
761
|
# * {IMAP URLAUTH Authorization Mechanism Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml]
|
720
762
|
#
|
721
763
|
class IMAP < Protocol
|
722
|
-
VERSION = "0.4.
|
764
|
+
VERSION = "0.4.20"
|
723
765
|
|
724
766
|
# Aliases for supported capabilities, to be used with the #enable command.
|
725
767
|
ENABLE_ALIASES = {
|
@@ -727,6 +769,7 @@ module Net
|
|
727
769
|
"UTF8=ONLY" => "UTF8=ACCEPT",
|
728
770
|
}.freeze
|
729
771
|
|
772
|
+
autoload :ResponseReader, File.expand_path("imap/response_reader", __dir__)
|
730
773
|
autoload :SASL, File.expand_path("imap/sasl", __dir__)
|
731
774
|
autoload :SASLAdapter, File.expand_path("imap/sasl_adapter", __dir__)
|
732
775
|
autoload :StringPrep, File.expand_path("imap/stringprep", __dir__)
|
@@ -741,9 +784,11 @@ module Net
|
|
741
784
|
def self.config; Config.global end
|
742
785
|
|
743
786
|
# Returns the global debug mode.
|
787
|
+
# Delegates to {Net::IMAP.config.debug}[rdoc-ref:Config#debug].
|
744
788
|
def self.debug; config.debug end
|
745
789
|
|
746
790
|
# Sets the global debug mode.
|
791
|
+
# Delegates to {Net::IMAP.config.debug=}[rdoc-ref:Config#debug=].
|
747
792
|
def self.debug=(val)
|
748
793
|
config.debug = val
|
749
794
|
end
|
@@ -764,7 +809,7 @@ module Net
|
|
764
809
|
alias default_ssl_port default_tls_port
|
765
810
|
end
|
766
811
|
|
767
|
-
# Returns the initial greeting the server, an UntaggedResponse.
|
812
|
+
# Returns the initial greeting sent by the server, an UntaggedResponse.
|
768
813
|
attr_reader :greeting
|
769
814
|
|
770
815
|
# The client configuration. See Net::IMAP::Config.
|
@@ -773,13 +818,28 @@ module Net
|
|
773
818
|
# Net::IMAP.config.
|
774
819
|
attr_reader :config
|
775
820
|
|
776
|
-
|
777
|
-
#
|
778
|
-
#
|
779
|
-
|
821
|
+
##
|
822
|
+
# :attr_reader: open_timeout
|
823
|
+
# Seconds to wait until a connection is opened. Also used by #starttls.
|
824
|
+
# Delegates to {config.open_timeout}[rdoc-ref:Config#open_timeout].
|
780
825
|
|
826
|
+
##
|
827
|
+
# :attr_reader: idle_response_timeout
|
781
828
|
# Seconds to wait until an IDLE response is received.
|
782
|
-
|
829
|
+
# Delegates to {config.idle_response_timeout}[rdoc-ref:Config#idle_response_timeout].
|
830
|
+
|
831
|
+
##
|
832
|
+
# :attr_accessor: max_response_size
|
833
|
+
#
|
834
|
+
# The maximum allowed server response size, in bytes.
|
835
|
+
# Delegates to {config.max_response_size}[rdoc-ref:Config#max_response_size].
|
836
|
+
|
837
|
+
# :stopdoc:
|
838
|
+
def open_timeout; config.open_timeout end
|
839
|
+
def idle_response_timeout; config.idle_response_timeout end
|
840
|
+
def max_response_size; config.max_response_size end
|
841
|
+
def max_response_size=(val) config.max_response_size = val end
|
842
|
+
# :startdoc:
|
783
843
|
|
784
844
|
# The hostname this client connected to
|
785
845
|
attr_reader :host
|
@@ -835,6 +895,12 @@ module Net
|
|
835
895
|
#
|
836
896
|
# See DeprecatedClientOptions.new for deprecated SSL arguments.
|
837
897
|
#
|
898
|
+
# [response_handlers]
|
899
|
+
# A list of response handlers to be added before the receiver thread is
|
900
|
+
# started. This ensures every server response is handled, including the
|
901
|
+
# #greeting. Note that the greeting is handled in the current thread, but
|
902
|
+
# all other responses are handled in the receiver thread.
|
903
|
+
#
|
838
904
|
# [config]
|
839
905
|
# A Net::IMAP::Config object to use as the basis for #config. By default,
|
840
906
|
# the global Net::IMAP.config is used.
|
@@ -906,7 +972,7 @@ module Net
|
|
906
972
|
# [Net::IMAP::ByeResponseError]
|
907
973
|
# Connected to the host successfully, but it immediately said goodbye.
|
908
974
|
#
|
909
|
-
def initialize(host, port: nil, ssl:
|
975
|
+
def initialize(host, port: nil, ssl: nil, response_handlers: nil,
|
910
976
|
config: Config.global, **config_options)
|
911
977
|
super()
|
912
978
|
# Config options
|
@@ -929,6 +995,7 @@ module Net
|
|
929
995
|
@receiver_thread = nil
|
930
996
|
@receiver_thread_exception = nil
|
931
997
|
@receiver_thread_terminating = false
|
998
|
+
response_handlers&.each do add_response_handler(_1) end
|
932
999
|
|
933
1000
|
# Client Protocol Sender (including state for currently running commands)
|
934
1001
|
@tag_prefix = "RUBY"
|
@@ -944,6 +1011,7 @@ module Net
|
|
944
1011
|
# Connection
|
945
1012
|
@tls_verified = false
|
946
1013
|
@sock = tcp_socket(@host, @port)
|
1014
|
+
@reader = ResponseReader.new(self, @sock)
|
947
1015
|
start_tls_session if ssl_ctx
|
948
1016
|
start_imap_connection
|
949
1017
|
|
@@ -1204,6 +1272,10 @@ module Net
|
|
1204
1272
|
# both successful. Any error indicates that the connection has not been
|
1205
1273
|
# secured.
|
1206
1274
|
#
|
1275
|
+
# After the server agrees to start a TLS connection, this method waits up to
|
1276
|
+
# {config.open_timeout}[rdoc-ref:Config#open_timeout] before raising
|
1277
|
+
# +Net::OpenTimeout+.
|
1278
|
+
#
|
1207
1279
|
# *Note:*
|
1208
1280
|
# >>>
|
1209
1281
|
# Any #response_handlers added before STARTTLS should be aware that the
|
@@ -1222,13 +1294,21 @@ module Net
|
|
1222
1294
|
#
|
1223
1295
|
def starttls(**options)
|
1224
1296
|
@ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options)
|
1225
|
-
|
1297
|
+
error = nil
|
1298
|
+
ok = send_command("STARTTLS") do |resp|
|
1226
1299
|
if resp.kind_of?(TaggedResponse) && resp.name == "OK"
|
1227
1300
|
clear_cached_capabilities
|
1228
1301
|
clear_responses
|
1229
1302
|
start_tls_session
|
1230
1303
|
end
|
1304
|
+
rescue Exception => error
|
1305
|
+
raise # note that the error backtrace is in the receiver_thread
|
1231
1306
|
end
|
1307
|
+
if error
|
1308
|
+
disconnect
|
1309
|
+
raise error
|
1310
|
+
end
|
1311
|
+
ok
|
1232
1312
|
end
|
1233
1313
|
|
1234
1314
|
# :call-seq:
|
@@ -2698,6 +2778,10 @@ module Net
|
|
2698
2778
|
# end
|
2699
2779
|
# }
|
2700
2780
|
#
|
2781
|
+
# Response handlers can also be added when the client is created before the
|
2782
|
+
# receiver thread is started, by the +response_handlers+ argument to ::new.
|
2783
|
+
# This ensures every server response is handled, including the #greeting.
|
2784
|
+
#
|
2701
2785
|
# Related: #remove_response_handler, #response_handlers
|
2702
2786
|
def add_response_handler(handler = nil, &block)
|
2703
2787
|
raise ArgumentError, "two Procs are passed" if handler && block
|
@@ -2724,6 +2808,7 @@ module Net
|
|
2724
2808
|
def start_imap_connection
|
2725
2809
|
@greeting = get_server_greeting
|
2726
2810
|
@capabilities = capabilities_from_resp_code @greeting
|
2811
|
+
@response_handlers.each do |handler| handler.call(@greeting) end
|
2727
2812
|
@receiver_thread = start_receiver_thread
|
2728
2813
|
rescue Exception
|
2729
2814
|
@sock.close
|
@@ -2852,23 +2937,10 @@ module Net
|
|
2852
2937
|
end
|
2853
2938
|
|
2854
2939
|
def get_response
|
2855
|
-
buff =
|
2856
|
-
while true
|
2857
|
-
s = @sock.gets(CRLF)
|
2858
|
-
break unless s
|
2859
|
-
buff.concat(s)
|
2860
|
-
if /\{(\d+)\}\r\n/n =~ s
|
2861
|
-
s = @sock.read($1.to_i)
|
2862
|
-
buff.concat(s)
|
2863
|
-
else
|
2864
|
-
break
|
2865
|
-
end
|
2866
|
-
end
|
2940
|
+
buff = @reader.read_response_buffer
|
2867
2941
|
return nil if buff.length == 0
|
2868
|
-
if config.debug?
|
2869
|
-
|
2870
|
-
end
|
2871
|
-
return @parser.parse(buff)
|
2942
|
+
$stderr.print(buff.gsub(/^/n, "S: ")) if config.debug?
|
2943
|
+
@parser.parse(buff)
|
2872
2944
|
end
|
2873
2945
|
|
2874
2946
|
#############################
|
@@ -3069,6 +3141,7 @@ module Net
|
|
3069
3141
|
raise "already using SSL" if @sock.kind_of?(OpenSSL::SSL::SSLSocket)
|
3070
3142
|
raise "cannot start TLS without SSLContext" unless ssl_ctx
|
3071
3143
|
@sock = SSLSocket.new(@sock, ssl_ctx)
|
3144
|
+
@reader = ResponseReader.new(self, @sock)
|
3072
3145
|
@sock.sync_close = true
|
3073
3146
|
@sock.hostname = @host if @sock.respond_to? :hostname=
|
3074
3147
|
ssl_socket_connect(@sock, open_timeout)
|