net-imap 0.4.20 → 0.5.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.
Potentially problematic release.
This version of net-imap might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Gemfile +7 -1
- data/lib/net/imap/authenticators.rb +2 -2
- data/lib/net/imap/command_data.rb +11 -0
- data/lib/net/imap/config/attr_type_coercion.rb +21 -25
- data/lib/net/imap/config.rb +38 -162
- data/lib/net/imap/data_encoding.rb +3 -3
- data/lib/net/imap/deprecated_client_options.rb +6 -3
- data/lib/net/imap/errors.rb +6 -33
- data/lib/net/imap/response_data.rb +60 -96
- data/lib/net/imap/response_parser.rb +18 -45
- data/lib/net/imap/sasl/authentication_exchange.rb +52 -20
- data/lib/net/imap/sasl/authenticators.rb +8 -4
- data/lib/net/imap/sasl/client_adapter.rb +77 -26
- data/lib/net/imap/sasl/cram_md5_authenticator.rb +1 -1
- data/lib/net/imap/sasl/digest_md5_authenticator.rb +213 -51
- data/lib/net/imap/sasl/login_authenticator.rb +2 -1
- data/lib/net/imap/sasl/protocol_adapters.rb +60 -4
- data/lib/net/imap/sasl.rb +6 -3
- data/lib/net/imap/sasl_adapter.rb +0 -1
- data/lib/net/imap/sequence_set.rb +128 -273
- data/lib/net/imap.rb +68 -159
- data/net-imap.gemspec +1 -1
- metadata +7 -6
- data/lib/net/imap/response_reader.rb +0 -75
- data/lib/net/imap/uidplus_data.rb +0 -326
@@ -5,9 +5,6 @@ module Net
|
|
5
5
|
autoload :FetchData, "#{__dir__}/fetch_data"
|
6
6
|
autoload :SearchResult, "#{__dir__}/search_result"
|
7
7
|
autoload :SequenceSet, "#{__dir__}/sequence_set"
|
8
|
-
autoload :UIDPlusData, "#{__dir__}/uidplus_data"
|
9
|
-
autoload :AppendUIDData, "#{__dir__}/uidplus_data"
|
10
|
-
autoload :CopyUIDData, "#{__dir__}/uidplus_data"
|
11
8
|
|
12
9
|
# Net::IMAP::ContinuationRequest represents command continuation requests.
|
13
10
|
#
|
@@ -327,6 +324,60 @@ module Net
|
|
327
324
|
# code data can take.
|
328
325
|
end
|
329
326
|
|
327
|
+
# Net::IMAP::UIDPlusData represents the ResponseCode#data that accompanies
|
328
|
+
# the +APPENDUID+ and +COPYUID+ response codes.
|
329
|
+
#
|
330
|
+
# See [[UIDPLUS[https://www.rfc-editor.org/rfc/rfc4315.html]].
|
331
|
+
#
|
332
|
+
# ==== Capability requirement
|
333
|
+
#
|
334
|
+
# The +UIDPLUS+ capability[rdoc-ref:Net::IMAP#capability] must be supported.
|
335
|
+
# A server that supports +UIDPLUS+ should send a UIDPlusData object inside
|
336
|
+
# every TaggedResponse returned by the append[rdoc-ref:Net::IMAP#append],
|
337
|
+
# copy[rdoc-ref:Net::IMAP#copy], move[rdoc-ref:Net::IMAP#move], {uid
|
338
|
+
# copy}[rdoc-ref:Net::IMAP#uid_copy], and {uid
|
339
|
+
# move}[rdoc-ref:Net::IMAP#uid_move] commands---unless the destination
|
340
|
+
# mailbox reports +UIDNOTSTICKY+.
|
341
|
+
#
|
342
|
+
#--
|
343
|
+
# TODO: support MULTIAPPEND
|
344
|
+
#++
|
345
|
+
#
|
346
|
+
class UIDPlusData < Struct.new(:uidvalidity, :source_uids, :assigned_uids)
|
347
|
+
##
|
348
|
+
# method: uidvalidity
|
349
|
+
# :call-seq: uidvalidity -> nonzero uint32
|
350
|
+
#
|
351
|
+
# The UIDVALIDITY of the destination mailbox.
|
352
|
+
|
353
|
+
##
|
354
|
+
# method: source_uids
|
355
|
+
# :call-seq: source_uids -> nil or an array of nonzero uint32
|
356
|
+
#
|
357
|
+
# The UIDs of the copied or moved messages.
|
358
|
+
#
|
359
|
+
# Note:: Returns +nil+ for Net::IMAP#append.
|
360
|
+
|
361
|
+
##
|
362
|
+
# method: assigned_uids
|
363
|
+
# :call-seq: assigned_uids -> an array of nonzero uint32
|
364
|
+
#
|
365
|
+
# The newly assigned UIDs of the copied, moved, or appended messages.
|
366
|
+
#
|
367
|
+
# Note:: This always returns an array, even when it contains only one UID.
|
368
|
+
|
369
|
+
##
|
370
|
+
# :call-seq: uid_mapping -> nil or a hash
|
371
|
+
#
|
372
|
+
# Returns a hash mapping each source UID to the newly assigned destination
|
373
|
+
# UID.
|
374
|
+
#
|
375
|
+
# Note:: Returns +nil+ for Net::IMAP#append.
|
376
|
+
def uid_mapping
|
377
|
+
source_uids&.zip(assigned_uids)&.to_h
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
330
381
|
# Net::IMAP::MailboxList represents contents of the LIST response,
|
331
382
|
# representing a single mailbox path.
|
332
383
|
#
|
@@ -888,7 +939,8 @@ module Net
|
|
888
939
|
# for something else?
|
889
940
|
#++
|
890
941
|
def media_subtype
|
891
|
-
warn("media_subtype is obsolete, use subtype instead.\n",
|
942
|
+
warn("media_subtype is obsolete, use subtype instead.\n",
|
943
|
+
uplevel: 1, category: :deprecated)
|
892
944
|
return subtype
|
893
945
|
end
|
894
946
|
end
|
@@ -933,7 +985,8 @@ module Net
|
|
933
985
|
# generate a warning message to +stderr+, then return
|
934
986
|
# the value of +subtype+.
|
935
987
|
def media_subtype
|
936
|
-
warn("media_subtype is obsolete, use subtype instead.\n",
|
988
|
+
warn("media_subtype is obsolete, use subtype instead.\n",
|
989
|
+
uplevel: 1, category: :deprecated)
|
937
990
|
return subtype
|
938
991
|
end
|
939
992
|
end
|
@@ -989,77 +1042,6 @@ module Net
|
|
989
1042
|
end
|
990
1043
|
end
|
991
1044
|
|
992
|
-
# BodyTypeAttachment is not used and will be removed in an upcoming release.
|
993
|
-
#
|
994
|
-
# === Bug Analysis
|
995
|
-
#
|
996
|
-
# \IMAP body structures are parenthesized lists and assign their fields
|
997
|
-
# positionally, so missing fields change the interpretation of all
|
998
|
-
# following fields. Additionally, different body types have a different
|
999
|
-
# number of required fields, followed by optional "extension" fields.
|
1000
|
-
#
|
1001
|
-
# BodyTypeAttachment was previously returned when a "message/rfc822" part,
|
1002
|
-
# which should be sent as <tt>body-type-msg</tt> with ten required fields,
|
1003
|
-
# was actually sent as a <tt>body-type-basic</tt> with _seven_ required
|
1004
|
-
# fields.
|
1005
|
-
#
|
1006
|
-
# basic => type, subtype, param, id, desc, enc, octets, md5=nil, dsp=nil, lang=nil, loc=nil, *ext
|
1007
|
-
# msg => type, subtype, param, id, desc, enc, octets, envelope, body, lines, md5=nil, ...
|
1008
|
-
#
|
1009
|
-
# Normally, +envelope+ and +md5+ are incompatible, but Net::IMAP leniently
|
1010
|
-
# allowed buggy servers to send +NIL+ for +envelope+. As a result, when a
|
1011
|
-
# server sent a <tt>message/rfc822</tt> part with +NIL+ for +md5+ and a
|
1012
|
-
# non-<tt>NIL</tt> +dsp+, Net::IMAP misinterpreted the
|
1013
|
-
# <tt>Content-Disposition</tt> as if it were a strange body type. In all
|
1014
|
-
# reported cases, the <tt>Content-Disposition</tt> was "attachment", so
|
1015
|
-
# BodyTypeAttachment was created as the workaround.
|
1016
|
-
#
|
1017
|
-
# === Current behavior
|
1018
|
-
#
|
1019
|
-
# When interpreted strictly, +envelope+ and +md5+ are incompatible. So the
|
1020
|
-
# current parsing algorithm peeks ahead after it has received the seventh
|
1021
|
-
# body field. If the next token is not the start of an +envelope+, we assume
|
1022
|
-
# the server has incorrectly sent us a <tt>body-type-basic</tt> and return
|
1023
|
-
# BodyTypeBasic. As a result, what was previously BodyTypeMessage#body =>
|
1024
|
-
# BodyTypeAttachment is now BodyTypeBasic#disposition => ContentDisposition.
|
1025
|
-
#
|
1026
|
-
class BodyTypeAttachment < Struct.new(:dsp_type, :_unused_, :param)
|
1027
|
-
# *invalid for BodyTypeAttachment*
|
1028
|
-
def media_type
|
1029
|
-
warn(<<~WARN, uplevel: 1)
|
1030
|
-
BodyTypeAttachment#media_type is obsolete. Use dsp_type instead.
|
1031
|
-
WARN
|
1032
|
-
dsp_type
|
1033
|
-
end
|
1034
|
-
|
1035
|
-
# *invalid for BodyTypeAttachment*
|
1036
|
-
def subtype
|
1037
|
-
warn("BodyTypeAttachment#subtype is obsolete.\n", uplevel: 1)
|
1038
|
-
nil
|
1039
|
-
end
|
1040
|
-
|
1041
|
-
##
|
1042
|
-
# method: dsp_type
|
1043
|
-
# :call-seq: dsp_type -> string
|
1044
|
-
#
|
1045
|
-
# Returns the content disposition type, as defined by
|
1046
|
-
# [DISPOSITION[https://tools.ietf.org/html/rfc2183]].
|
1047
|
-
|
1048
|
-
##
|
1049
|
-
# method: param
|
1050
|
-
# :call-seq: param -> hash
|
1051
|
-
#
|
1052
|
-
# Returns a hash representing parameters of the Content-Disposition
|
1053
|
-
# field, as defined by [DISPOSITION[https://tools.ietf.org/html/rfc2183]].
|
1054
|
-
|
1055
|
-
##
|
1056
|
-
def multipart?
|
1057
|
-
return false
|
1058
|
-
end
|
1059
|
-
end
|
1060
|
-
|
1061
|
-
deprecate_constant :BodyTypeAttachment
|
1062
|
-
|
1063
1045
|
# Net::IMAP::BodyTypeMultipart represents body structures of messages and
|
1064
1046
|
# message parts, when <tt>Content-Type</tt> is <tt>multipart/*</tt>.
|
1065
1047
|
class BodyTypeMultipart < Struct.new(:media_type, :subtype,
|
@@ -1131,29 +1113,11 @@ module Net
|
|
1131
1113
|
# generate a warning message to +stderr+, then return
|
1132
1114
|
# the value of +subtype+.
|
1133
1115
|
def media_subtype
|
1134
|
-
warn("media_subtype is obsolete, use subtype instead.\n",
|
1116
|
+
warn("media_subtype is obsolete, use subtype instead.\n",
|
1117
|
+
uplevel: 1, category: :deprecated)
|
1135
1118
|
return subtype
|
1136
1119
|
end
|
1137
1120
|
end
|
1138
1121
|
|
1139
|
-
# === Obsolete
|
1140
|
-
# BodyTypeExtension is not used and will be removed in an upcoming release.
|
1141
|
-
#
|
1142
|
-
# >>>
|
1143
|
-
# BodyTypeExtension was (incorrectly) used for <tt>message/*</tt> parts
|
1144
|
-
# (besides <tt>message/rfc822</tt>, which correctly uses BodyTypeMessage).
|
1145
|
-
#
|
1146
|
-
# Net::IMAP now (correctly) parses all message types (other than
|
1147
|
-
# <tt>message/rfc822</tt> or <tt>message/global</tt>) as BodyTypeBasic.
|
1148
|
-
class BodyTypeExtension < Struct.new(:media_type, :subtype,
|
1149
|
-
:params, :content_id,
|
1150
|
-
:description, :encoding, :size)
|
1151
|
-
def multipart?
|
1152
|
-
return false
|
1153
|
-
end
|
1154
|
-
end
|
1155
|
-
|
1156
|
-
deprecate_constant :BodyTypeExtension
|
1157
|
-
|
1158
1122
|
end
|
1159
1123
|
end
|
@@ -13,17 +13,13 @@ module Net
|
|
13
13
|
|
14
14
|
attr_reader :config
|
15
15
|
|
16
|
-
#
|
17
|
-
#
|
18
|
-
# When +config+ is frozen or global, the parser #config inherits from it.
|
19
|
-
# Otherwise, +config+ will be used directly.
|
16
|
+
# :call-seq: Net::IMAP::ResponseParser.new -> Net::IMAP::ResponseParser
|
20
17
|
def initialize(config: Config.global)
|
21
18
|
@str = nil
|
22
19
|
@pos = nil
|
23
20
|
@lex_state = nil
|
24
21
|
@token = nil
|
25
22
|
@config = Config[config]
|
26
|
-
@config = @config.new if @config == Config.global || @config.frozen?
|
27
23
|
end
|
28
24
|
|
29
25
|
# :call-seq:
|
@@ -1321,31 +1317,19 @@ module Net
|
|
1321
1317
|
# header-fld-name = astring
|
1322
1318
|
#
|
1323
1319
|
# NOTE: Previously, Net::IMAP recreated the raw original source string.
|
1324
|
-
# Now, it
|
1325
|
-
#
|
1326
|
-
#
|
1327
|
-
# standard header field names are valid atoms:
|
1320
|
+
# Now, it returns the decoded astring value. Although this is technically
|
1321
|
+
# incompatible, it should almost never make a difference: all standard
|
1322
|
+
# header field names are valid atoms:
|
1328
1323
|
#
|
1329
1324
|
# https://www.iana.org/assignments/message-headers/message-headers.xhtml
|
1330
1325
|
#
|
1331
|
-
#
|
1332
|
-
# or more of the printable US-ASCII characters, except SP and colon. So
|
1333
|
-
# empty string isn't valid, and literals aren't needed and should not be
|
1334
|
-
# used. This is explicitly unchanged by [I18N-HDRS] (RFC6532).
|
1335
|
-
#
|
1336
|
-
# RFC5233:
|
1326
|
+
# See also RFC5233:
|
1337
1327
|
# optional-field = field-name ":" unstructured CRLF
|
1338
1328
|
# field-name = 1*ftext
|
1339
1329
|
# ftext = %d33-57 / ; Printable US-ASCII
|
1340
1330
|
# %d59-126 ; characters not including
|
1341
1331
|
# ; ":".
|
1342
|
-
|
1343
|
-
assert_no_lookahead
|
1344
|
-
start = @pos
|
1345
|
-
astring
|
1346
|
-
end_pos = @token ? @pos - 1 : @pos
|
1347
|
-
@str[start...end_pos]
|
1348
|
-
end
|
1332
|
+
alias header_fld_name astring
|
1349
1333
|
|
1350
1334
|
# mailbox-data = "FLAGS" SP flag-list / "LIST" SP mailbox-list /
|
1351
1335
|
# "LSUB" SP mailbox-list / "SEARCH" *(SP nz-number) /
|
@@ -1867,10 +1851,11 @@ module Net
|
|
1867
1851
|
#
|
1868
1852
|
# n.b, uniqueid ⊂ uid-set. To avoid inconsistent return types, we always
|
1869
1853
|
# match uid_set even if that returns a single-member array.
|
1854
|
+
#
|
1870
1855
|
def resp_code_apnd__data
|
1871
1856
|
validity = number; SP!
|
1872
1857
|
dst_uids = uid_set # uniqueid ⊂ uid-set
|
1873
|
-
|
1858
|
+
UIDPlusData.new(validity, nil, dst_uids)
|
1874
1859
|
end
|
1875
1860
|
|
1876
1861
|
# already matched: "COPYUID"
|
@@ -1880,25 +1865,7 @@ module Net
|
|
1880
1865
|
validity = number; SP!
|
1881
1866
|
src_uids = uid_set; SP!
|
1882
1867
|
dst_uids = uid_set
|
1883
|
-
|
1884
|
-
end
|
1885
|
-
|
1886
|
-
def AppendUID(...) DeprecatedUIDPlus(...) || AppendUIDData.new(...) end
|
1887
|
-
def CopyUID(...) DeprecatedUIDPlus(...) || CopyUIDData.new(...) end
|
1888
|
-
|
1889
|
-
# TODO: remove this code in the v0.6.0 release
|
1890
|
-
def DeprecatedUIDPlus(validity, src_uids = nil, dst_uids)
|
1891
|
-
return unless config.parser_use_deprecated_uidplus_data
|
1892
|
-
compact_uid_sets = [src_uids, dst_uids].compact
|
1893
|
-
count = compact_uid_sets.map { _1.count_with_duplicates }.max
|
1894
|
-
max = config.parser_max_deprecated_uidplus_data_size
|
1895
|
-
if count <= max
|
1896
|
-
src_uids &&= src_uids.each_ordered_number.to_a
|
1897
|
-
dst_uids = dst_uids.each_ordered_number.to_a
|
1898
|
-
UIDPlusData.new(validity, src_uids, dst_uids)
|
1899
|
-
elsif config.parser_use_deprecated_uidplus_data != :up_to_max_size
|
1900
|
-
parse_error("uid-set is too large: %d > %d", count, max)
|
1901
|
-
end
|
1868
|
+
UIDPlusData.new(validity, src_uids, dst_uids)
|
1902
1869
|
end
|
1903
1870
|
|
1904
1871
|
ADDRESS_REGEXP = /\G
|
@@ -2024,9 +1991,15 @@ module Net
|
|
2024
1991
|
# uniqueid = nz-number
|
2025
1992
|
# ; Strictly ascending
|
2026
1993
|
def uid_set
|
2027
|
-
|
2028
|
-
|
2029
|
-
|
1994
|
+
token = match(T_NUMBER, T_ATOM)
|
1995
|
+
case token.symbol
|
1996
|
+
when T_NUMBER then [Integer(token.value)]
|
1997
|
+
when T_ATOM
|
1998
|
+
token.value.split(",").flat_map {|range|
|
1999
|
+
range = range.split(":").map {|uniqueid| Integer(uniqueid) }
|
2000
|
+
range.size == 1 ? range : Range.new(range.min, range.max).to_a
|
2001
|
+
}
|
2002
|
+
end
|
2030
2003
|
end
|
2031
2004
|
|
2032
2005
|
def nil_atom
|
@@ -4,44 +4,76 @@ module Net
|
|
4
4
|
class IMAP
|
5
5
|
module SASL
|
6
6
|
|
7
|
-
#
|
7
|
+
# AuthenticationExchange is used internally by Net::IMAP#authenticate.
|
8
|
+
# But the API is still *experimental*, and may change.
|
8
9
|
#
|
9
10
|
# TODO: catch exceptions in #process and send #cancel_response.
|
10
11
|
# TODO: raise an error if the command succeeds after being canceled.
|
11
12
|
# TODO: use with more clients, to verify the API can accommodate them.
|
13
|
+
# TODO: pass ClientAdapter#service to SASL.authenticator
|
12
14
|
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
# ).authenticate
|
20
|
-
# end
|
21
|
-
#
|
22
|
-
# private
|
15
|
+
# An AuthenticationExchange represents a single attempt to authenticate
|
16
|
+
# a SASL client to a SASL server. It is created from a client adapter, a
|
17
|
+
# mechanism name, and a mechanism authenticator. When #authenticate is
|
18
|
+
# called, it will send the appropriate authenticate command to the server,
|
19
|
+
# returning the client response on success and raising an exception on
|
20
|
+
# failure.
|
23
21
|
#
|
24
|
-
#
|
22
|
+
# In most cases, the client will not need to use
|
23
|
+
# SASL::AuthenticationExchange directly at all. Instead, use
|
24
|
+
# SASL::ClientAdapter#authenticate. If customizations are needed, the
|
25
|
+
# custom client adapter is probably the best place for that code.
|
25
26
|
#
|
26
|
-
# Or delegate creation of the authenticator to ::build:
|
27
27
|
# def authenticate(...)
|
28
|
-
#
|
29
|
-
# .authenticate
|
28
|
+
# MyClient::SASLAdapter.new(self).authenticate(...)
|
30
29
|
# end
|
31
30
|
#
|
32
|
-
#
|
31
|
+
# SASL::ClientAdapter#authenticate delegates to ::authenticate, like so:
|
32
|
+
#
|
33
33
|
# def authenticate(...)
|
34
|
+
# sasl_adapter = MyClient::SASLAdapter.new(self)
|
34
35
|
# SASL::AuthenticationExchange.authenticate(sasl_adapter, ...)
|
35
36
|
# end
|
36
37
|
#
|
37
|
-
#
|
38
|
-
#
|
38
|
+
# ::authenticate simply delegates to ::build and #authenticate, like so:
|
39
|
+
#
|
40
|
+
# def authenticate(...)
|
41
|
+
# sasl_adapter = MyClient::SASLAdapter.new(self)
|
42
|
+
# SASL::AuthenticationExchange
|
43
|
+
# .build(sasl_adapter, ...)
|
44
|
+
# .authenticate
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# And ::build delegates to SASL.authenticator and ::new, like so:
|
48
|
+
#
|
49
|
+
# def authenticate(mechanism, ...)
|
50
|
+
# sasl_adapter = MyClient::SASLAdapter.new(self)
|
51
|
+
# authenticator = SASL.authenticator(mechanism, ...)
|
52
|
+
# SASL::AuthenticationExchange
|
53
|
+
# .new(sasl_adapter, mechanism, authenticator)
|
54
|
+
# .authenticate
|
55
|
+
# end
|
39
56
|
#
|
40
57
|
class AuthenticationExchange
|
41
58
|
# Convenience method for <tt>build(...).authenticate</tt>
|
59
|
+
#
|
60
|
+
# See also: SASL::ClientAdapter#authenticate
|
42
61
|
def self.authenticate(...) build(...).authenticate end
|
43
62
|
|
44
|
-
#
|
63
|
+
# Convenience method to combine the creation of a new authenticator and
|
64
|
+
# a new Authentication exchange.
|
65
|
+
#
|
66
|
+
# +client+ must be an instance of SASL::ClientAdapter.
|
67
|
+
#
|
68
|
+
# +mechanism+ must be a SASL mechanism name, as a string or symbol.
|
69
|
+
#
|
70
|
+
# +sasl_ir+ allows or disallows sending an "initial response", depending
|
71
|
+
# also on whether the server capabilities, mechanism authenticator, and
|
72
|
+
# client adapter all support it. Defaults to +true+.
|
73
|
+
#
|
74
|
+
# +mechanism+, +args+, +kwargs+, and +block+ are all forwarded to
|
75
|
+
# SASL.authenticator. Use the +registry+ kwarg to override the global
|
76
|
+
# SASL::Authenticators registry.
|
45
77
|
def self.build(client, mechanism, *args, sasl_ir: true, **kwargs, &block)
|
46
78
|
authenticator = SASL.authenticator(mechanism, *args, **kwargs, &block)
|
47
79
|
new(client, mechanism, authenticator, sasl_ir: sasl_ir)
|
@@ -51,7 +83,7 @@ module Net
|
|
51
83
|
|
52
84
|
def initialize(client, mechanism, authenticator, sasl_ir: true)
|
53
85
|
@client = client
|
54
|
-
@mechanism =
|
86
|
+
@mechanism = Authenticators.normalize_name(mechanism)
|
55
87
|
@authenticator = authenticator
|
56
88
|
@sasl_ir = sasl_ir
|
57
89
|
@processed = false
|
@@ -21,6 +21,10 @@ module Net::IMAP::SASL
|
|
21
21
|
# ScramSHA1Authenticator for examples.
|
22
22
|
class Authenticators
|
23
23
|
|
24
|
+
# Normalize the mechanism name as an uppercase string, with underscores
|
25
|
+
# converted to dashes.
|
26
|
+
def self.normalize_name(mechanism) -(mechanism.to_s.upcase.tr(?_, ?-)) end
|
27
|
+
|
24
28
|
# Create a new Authenticators registry.
|
25
29
|
#
|
26
30
|
# This class is usually not instantiated directly. Use SASL.authenticators
|
@@ -65,7 +69,6 @@ module Net::IMAP::SASL
|
|
65
69
|
# lazily loaded from <tt>Net::IMAP::SASL::#{name}Authenticator</tt> (case is
|
66
70
|
# preserved and non-alphanumeric characters are removed..
|
67
71
|
def add_authenticator(name, authenticator = nil)
|
68
|
-
key = -name.to_s.upcase.tr(?_, ?-)
|
69
72
|
authenticator ||= begin
|
70
73
|
class_name = "#{name.gsub(/[^a-zA-Z0-9]/, "")}Authenticator".to_sym
|
71
74
|
auth_class = nil
|
@@ -74,17 +77,18 @@ module Net::IMAP::SASL
|
|
74
77
|
auth_class.new(*creds, **props, &block)
|
75
78
|
}
|
76
79
|
end
|
80
|
+
key = Authenticators.normalize_name(name)
|
77
81
|
@authenticators[key] = authenticator
|
78
82
|
end
|
79
83
|
|
80
84
|
# Removes the authenticator registered for +name+
|
81
85
|
def remove_authenticator(name)
|
82
|
-
key =
|
86
|
+
key = Authenticators.normalize_name(name)
|
83
87
|
@authenticators.delete(key)
|
84
88
|
end
|
85
89
|
|
86
90
|
def mechanism?(name)
|
87
|
-
key =
|
91
|
+
key = Authenticators.normalize_name(name)
|
88
92
|
@authenticators.key?(key)
|
89
93
|
end
|
90
94
|
|
@@ -105,7 +109,7 @@ module Net::IMAP::SASL
|
|
105
109
|
# only. Protocol client users should see refer to their client's
|
106
110
|
# documentation, e.g. Net::IMAP#authenticate.
|
107
111
|
def authenticator(mechanism, ...)
|
108
|
-
key =
|
112
|
+
key = Authenticators.normalize_name(mechanism)
|
109
113
|
auth = @authenticators.fetch(key) do
|
110
114
|
raise ArgumentError, 'unknown auth type - "%s"' % key
|
111
115
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "forwardable"
|
4
|
+
|
3
5
|
module Net
|
4
6
|
class IMAP
|
5
7
|
module SASL
|
@@ -8,42 +10,76 @@ module Net
|
|
8
10
|
#
|
9
11
|
# TODO: use with more clients, to verify the API can accommodate them.
|
10
12
|
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
# to match
|
13
|
+
# Represents the client to a SASL::AuthenticationExchange. By default,
|
14
|
+
# most methods simply delegate to #client. Clients should subclass
|
15
|
+
# SASL::ClientAdapter and override methods as needed to match the
|
16
|
+
# semantics of this API to their API.
|
14
17
|
#
|
15
|
-
#
|
16
|
-
# will probably need to override some methods. Additionally, subclasses
|
17
|
-
# may need to include a protocol adapter mixin, if the default
|
18
|
+
# Subclasses should also include a protocol adapter mixin when the default
|
18
19
|
# ProtocolAdapters::Generic isn't sufficient.
|
20
|
+
#
|
21
|
+
# === Protocol Requirements
|
22
|
+
#
|
23
|
+
# {RFC4422 §4}[https://www.rfc-editor.org/rfc/rfc4422.html#section-4]
|
24
|
+
# lists requirements for protocol specifications to offer SASL. Where
|
25
|
+
# possible, ClientAdapter delegates the handling of these requirements to
|
26
|
+
# SASL::ProtocolAdapters.
|
19
27
|
class ClientAdapter
|
28
|
+
extend Forwardable
|
29
|
+
|
20
30
|
include ProtocolAdapters::Generic
|
21
31
|
|
22
|
-
|
32
|
+
# The client that handles communication with the protocol server.
|
33
|
+
#
|
34
|
+
# Most ClientAdapter methods are simply delegated to #client by default.
|
35
|
+
attr_reader :client
|
23
36
|
|
24
37
|
# +command_proc+ can used to avoid exposing private methods on #client.
|
25
|
-
# It
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
38
|
+
# It's value is set by the block that is passed to ::new, and it is used
|
39
|
+
# by the default implementation of #run_command. Subclasses that
|
40
|
+
# override #run_command may use #command_proc for any other purpose they
|
41
|
+
# find useful.
|
29
42
|
#
|
30
|
-
#
|
31
|
-
#
|
43
|
+
# In the default implementation of #run_command, command_proc is called
|
44
|
+
# with the protocols authenticate +command+ name, the +mechanism+ name,
|
45
|
+
# an _optional_ +initial_response+ argument, and a +continuations+
|
46
|
+
# block. command_proc must run the protocol command with the arguments
|
47
|
+
# sent to it, _yield_ the payload of each continuation, respond to the
|
48
|
+
# continuation with the result of each _yield_, and _return_ the
|
49
|
+
# command's successful result. Non-successful results *MUST* raise
|
50
|
+
# an exception.
|
51
|
+
attr_reader :command_proc
|
52
|
+
|
53
|
+
# By default, this simply sets the #client and #command_proc attributes.
|
54
|
+
# Subclasses may override it, for example: to set the appropriate
|
55
|
+
# command_proc automatically.
|
32
56
|
def initialize(client, &command_proc)
|
33
57
|
@client, @command_proc = client, command_proc
|
34
58
|
end
|
35
59
|
|
36
|
-
#
|
60
|
+
# Attempt to authenticate #client to the server.
|
61
|
+
#
|
62
|
+
# By default, this simply delegates to
|
63
|
+
# AuthenticationExchange.authenticate.
|
37
64
|
def authenticate(...) AuthenticationExchange.authenticate(self, ...) end
|
38
65
|
|
39
|
-
|
40
|
-
|
66
|
+
##
|
67
|
+
# method: sasl_ir_capable?
|
68
|
+
# Do the protocol, server, and client all support an initial response?
|
69
|
+
def_delegator :client, :sasl_ir_capable?
|
41
70
|
|
42
|
-
|
43
|
-
|
71
|
+
##
|
72
|
+
# method: auth_capable?
|
73
|
+
# call-seq: auth_capable?(mechanism)
|
74
|
+
#
|
75
|
+
# Does the server advertise support for the +mechanism+?
|
76
|
+
def_delegator :client, :auth_capable?
|
44
77
|
|
45
|
-
#
|
46
|
-
#
|
78
|
+
# Calls command_proc with +command_name+ (see
|
79
|
+
# SASL::ProtocolAdapters::Generic#command_name),
|
80
|
+
# +mechanism+, +initial_response+, and a +continuations_handler+ block.
|
81
|
+
# The +initial_response+ is optional; when it's nil, it won't be sent to
|
82
|
+
# command_proc.
|
47
83
|
#
|
48
84
|
# Yields each continuation payload, responds to the server with the
|
49
85
|
# result of each yield, and returns the result. Non-successful results
|
@@ -51,21 +87,36 @@ module Net
|
|
51
87
|
# command to fail.
|
52
88
|
#
|
53
89
|
# Subclasses that override this may use #command_proc differently.
|
54
|
-
def run_command(mechanism, initial_response = nil, &
|
90
|
+
def run_command(mechanism, initial_response = nil, &continuations_handler)
|
55
91
|
command_proc or raise Error, "initialize with block or override"
|
56
92
|
args = [command_name, mechanism, initial_response].compact
|
57
|
-
command_proc.call(*args, &
|
93
|
+
command_proc.call(*args, &continuations_handler)
|
58
94
|
end
|
59
95
|
|
96
|
+
##
|
97
|
+
# method: host
|
98
|
+
# The hostname to which the client connected.
|
99
|
+
def_delegator :client, :host
|
100
|
+
|
101
|
+
##
|
102
|
+
# method: port
|
103
|
+
# The destination port to which the client connected.
|
104
|
+
def_delegator :client, :port
|
105
|
+
|
60
106
|
# Returns an array of server responses errors raised by run_command.
|
61
107
|
# Exceptions in this array won't drop the connection.
|
62
108
|
def response_errors; [] end
|
63
109
|
|
64
|
-
|
65
|
-
|
110
|
+
##
|
111
|
+
# method: drop_connection
|
112
|
+
# Drop the connection gracefully, sending a "LOGOUT" command as needed.
|
113
|
+
def_delegator :client, :drop_connection
|
114
|
+
|
115
|
+
##
|
116
|
+
# method: drop_connection!
|
117
|
+
# Drop the connection abruptly, closing the socket without logging out.
|
118
|
+
def_delegator :client, :drop_connection!
|
66
119
|
|
67
|
-
# Drop the connection abruptly.
|
68
|
-
def drop_connection!; client.drop_connection! end
|
69
120
|
end
|
70
121
|
end
|
71
122
|
end
|
@@ -20,7 +20,7 @@ class Net::IMAP::SASL::CramMD5Authenticator
|
|
20
20
|
warn_deprecation: true,
|
21
21
|
**)
|
22
22
|
if warn_deprecation
|
23
|
-
warn "WARNING: CRAM-MD5 mechanism is deprecated."
|
23
|
+
warn "WARNING: CRAM-MD5 mechanism is deprecated.", category: :deprecated
|
24
24
|
end
|
25
25
|
require "digest/md5"
|
26
26
|
@user = authcid || username || user
|