net-imap 0.4.12 → 0.5.5
Sign up to get free protection for your applications and to get access to all the features.
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/README.md +10 -4
- data/docs/styles.css +75 -14
- data/lib/net/imap/authenticators.rb +2 -2
- data/lib/net/imap/command_data.rb +61 -48
- data/lib/net/imap/config/attr_accessors.rb +75 -0
- data/lib/net/imap/config/attr_inheritance.rb +90 -0
- data/lib/net/imap/config/attr_type_coercion.rb +61 -0
- data/lib/net/imap/config.rb +402 -0
- data/lib/net/imap/data_encoding.rb +3 -3
- data/lib/net/imap/data_lite.rb +226 -0
- data/lib/net/imap/deprecated_client_options.rb +8 -5
- data/lib/net/imap/errors.rb +6 -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 +126 -193
- data/lib/net/imap/response_parser/parser_utils.rb +11 -6
- data/lib/net/imap/response_parser.rb +159 -21
- data/lib/net/imap/sasl/anonymous_authenticator.rb +3 -3
- 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 +4 -4
- data/lib/net/imap/sasl/digest_md5_authenticator.rb +218 -56
- 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 +4 -3
- 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/protocol_adapters.rb +60 -4
- data/lib/net/imap/sasl/scram_authenticator.rb +8 -8
- data/lib/net/imap/sasl.rb +7 -4
- data/lib/net/imap/sasl_adapter.rb +0 -1
- data/lib/net/imap/search_result.rb +2 -2
- data/lib/net/imap/sequence_set.rb +28 -24
- data/lib/net/imap/stringprep/nameprep.rb +1 -1
- data/lib/net/imap/stringprep/trace.rb +4 -4
- data/lib/net/imap/vanished_data.rb +56 -0
- data/lib/net/imap.rb +1001 -319
- data/net-imap.gemspec +3 -3
- data/rakelib/rfcs.rake +2 -0
- data/rakelib/string_prep_tables_generator.rb +2 -0
- metadata +11 -10
- data/.github/dependabot.yml +0 -6
- data/.github/workflows/pages.yml +0 -46
- data/.github/workflows/push_gem.yml +0 -48
- data/.github/workflows/test.yml +0 -31
- data/.gitignore +0 -12
- data/.mailmap +0 -13
@@ -154,7 +154,7 @@ module Net
|
|
154
154
|
end
|
155
155
|
|
156
156
|
# To be used conditionally:
|
157
|
-
# assert_no_lookahead if
|
157
|
+
# assert_no_lookahead if config.debug?
|
158
158
|
def assert_no_lookahead
|
159
159
|
@token.nil? or
|
160
160
|
parse_error("assertion failed: expected @token.nil?, actual %s: %p",
|
@@ -181,23 +181,28 @@ module Net
|
|
181
181
|
end
|
182
182
|
|
183
183
|
def peek_str?(str)
|
184
|
-
assert_no_lookahead if
|
184
|
+
assert_no_lookahead if config.debug?
|
185
185
|
@str[@pos, str.length] == str
|
186
186
|
end
|
187
187
|
|
188
|
-
def peek_re(re)
|
188
|
+
def peek_re?(re)
|
189
189
|
assert_no_lookahead if Net::IMAP.debug
|
190
|
+
re.match?(@str, @pos)
|
191
|
+
end
|
192
|
+
|
193
|
+
def peek_re(re)
|
194
|
+
assert_no_lookahead if config.debug?
|
190
195
|
re.match(@str, @pos)
|
191
196
|
end
|
192
197
|
|
193
198
|
def accept_re(re)
|
194
|
-
assert_no_lookahead if
|
199
|
+
assert_no_lookahead if config.debug?
|
195
200
|
re.match(@str, @pos) and @pos = $~.end(0)
|
196
201
|
$~
|
197
202
|
end
|
198
203
|
|
199
204
|
def match_re(re, name)
|
200
|
-
assert_no_lookahead if
|
205
|
+
assert_no_lookahead if config.debug?
|
201
206
|
if re.match(@str, @pos)
|
202
207
|
@pos = $~.end(0)
|
203
208
|
$~
|
@@ -212,7 +217,7 @@ module Net
|
|
212
217
|
|
213
218
|
def parse_error(fmt, *args)
|
214
219
|
msg = format(fmt, *args)
|
215
|
-
if
|
220
|
+
if config.debug?
|
216
221
|
local_path = File.dirname(__dir__)
|
217
222
|
tok = @token ? "%s: %p" % [@token.symbol, @token.value] : "nil"
|
218
223
|
warn "%s %s: %s" % [self.class, __method__, msg]
|
@@ -11,12 +11,15 @@ module Net
|
|
11
11
|
include ParserUtils
|
12
12
|
extend ParserUtils::Generator
|
13
13
|
|
14
|
+
attr_reader :config
|
15
|
+
|
14
16
|
# :call-seq: Net::IMAP::ResponseParser.new -> Net::IMAP::ResponseParser
|
15
|
-
def initialize
|
17
|
+
def initialize(config: Config.global)
|
16
18
|
@str = nil
|
17
19
|
@pos = nil
|
18
20
|
@lex_state = nil
|
19
21
|
@token = nil
|
22
|
+
@config = Config[config]
|
20
23
|
end
|
21
24
|
|
22
25
|
# :call-seq:
|
@@ -318,6 +321,24 @@ module Net
|
|
318
321
|
SEQUENCE_SET = /#{SEQUENCE_SET_ITEM}(?:,#{SEQUENCE_SET_ITEM})*/n
|
319
322
|
SEQUENCE_SET_STR = /\A#{SEQUENCE_SET}\z/n
|
320
323
|
|
324
|
+
# partial-range-first = nz-number ":" nz-number
|
325
|
+
# ;; Request to search from oldest (lowest UIDs) to
|
326
|
+
# ;; more recent messages.
|
327
|
+
# ;; A range 500:400 is the same as 400:500.
|
328
|
+
# ;; This is similar to <seq-range> from [RFC3501]
|
329
|
+
# ;; but cannot contain "*".
|
330
|
+
PARTIAL_RANGE_FIRST = /\A(#{NZ_NUMBER}):(#{NZ_NUMBER})\z/n
|
331
|
+
|
332
|
+
# partial-range-last = MINUS nz-number ":" MINUS nz-number
|
333
|
+
# ;; Request to search from newest (highest UIDs) to
|
334
|
+
# ;; oldest messages.
|
335
|
+
# ;; A range -500:-400 is the same as -400:-500.
|
336
|
+
PARTIAL_RANGE_LAST = /\A(-#{NZ_NUMBER}):(-#{NZ_NUMBER})\z/n
|
337
|
+
|
338
|
+
# partial-range = partial-range-first / partial-range-last
|
339
|
+
PARTIAL_RANGE = Regexp.union(PARTIAL_RANGE_FIRST,
|
340
|
+
PARTIAL_RANGE_LAST)
|
341
|
+
|
321
342
|
# RFC3501:
|
322
343
|
# literal = "{" number "}" CRLF *CHAR8
|
323
344
|
# ; Number represents the number of CHAR8s
|
@@ -713,7 +734,7 @@ module Net
|
|
713
734
|
when "EXISTS" then mailbox_data__exists # RFC3501, RFC9051
|
714
735
|
when "ESEARCH" then esearch_response # RFC4731, RFC9051, etc
|
715
736
|
when "VANISHED" then expunged_resp # RFC7162
|
716
|
-
when "UIDFETCH" then uidfetch_resp #
|
737
|
+
when "UIDFETCH" then uidfetch_resp # RFC9586
|
717
738
|
when "SEARCH" then mailbox_data__search # RFC3501 (obsolete)
|
718
739
|
when "CAPABILITY" then capability_data__untagged # RFC3501, RFC9051
|
719
740
|
when "FLAGS" then mailbox_data__flags # RFC3501, RFC9051
|
@@ -766,9 +787,6 @@ module Net
|
|
766
787
|
def response_data__ignored; response_data__unhandled(IgnoredResponse) end
|
767
788
|
alias response_data__noop response_data__ignored
|
768
789
|
|
769
|
-
alias esearch_response response_data__unhandled
|
770
|
-
alias expunged_resp response_data__unhandled
|
771
|
-
alias uidfetch_resp response_data__unhandled
|
772
790
|
alias listrights_data response_data__unhandled
|
773
791
|
alias myrights_data response_data__unhandled
|
774
792
|
alias metadata_resp response_data__unhandled
|
@@ -829,6 +847,14 @@ module Net
|
|
829
847
|
UntaggedResponse.new(name, data, @str)
|
830
848
|
end
|
831
849
|
|
850
|
+
# uidfetch-resp = uniqueid SP "UIDFETCH" SP msg-att
|
851
|
+
def uidfetch_resp
|
852
|
+
uid = uniqueid; SP!
|
853
|
+
name = label "UIDFETCH"; SP!
|
854
|
+
data = UIDFetchData.new(uid, msg_att(uid))
|
855
|
+
UntaggedResponse.new(name, data, @str)
|
856
|
+
end
|
857
|
+
|
832
858
|
def response_data__simple_numeric
|
833
859
|
data = nz_number; SP!
|
834
860
|
name = tagged_ext_label
|
@@ -839,6 +865,20 @@ module Net
|
|
839
865
|
alias mailbox_data__exists response_data__simple_numeric
|
840
866
|
alias mailbox_data__recent response_data__simple_numeric
|
841
867
|
|
868
|
+
# The name for this is confusing, because it *replaces* EXPUNGE
|
869
|
+
# >>>
|
870
|
+
# expunged-resp = "VANISHED" [SP "(EARLIER)"] SP known-uids
|
871
|
+
def expunged_resp
|
872
|
+
name = label "VANISHED"; SP!
|
873
|
+
earlier = if lpar? then label("EARLIER"); rpar; SP!; true else false end
|
874
|
+
uids = known_uids
|
875
|
+
data = VanishedData[uids, earlier]
|
876
|
+
UntaggedResponse.new name, data, @str
|
877
|
+
end
|
878
|
+
|
879
|
+
# TODO: replace with uid_set
|
880
|
+
alias known_uids sequence_set
|
881
|
+
|
842
882
|
# RFC3501 & RFC9051:
|
843
883
|
# msg-att = "(" (msg-att-dynamic / msg-att-static)
|
844
884
|
# *(SP (msg-att-dynamic / msg-att-static)) ")"
|
@@ -1314,30 +1354,19 @@ module Net
|
|
1314
1354
|
# header-fld-name = astring
|
1315
1355
|
#
|
1316
1356
|
# NOTE: Previously, Net::IMAP recreated the raw original source string.
|
1317
|
-
# Now, it
|
1318
|
-
#
|
1319
|
-
#
|
1320
|
-
# standard header field names are valid atoms:
|
1357
|
+
# Now, it returns the decoded astring value. Although this is technically
|
1358
|
+
# incompatible, it should almost never make a difference: all standard
|
1359
|
+
# header field names are valid atoms:
|
1321
1360
|
#
|
1322
1361
|
# https://www.iana.org/assignments/message-headers/message-headers.xhtml
|
1323
1362
|
#
|
1324
|
-
#
|
1325
|
-
# or more of the printable US-ASCII characters, except SP and colon. So
|
1326
|
-
# empty string isn't valid, and literals aren't needed and should not be
|
1327
|
-
# used. This is explicitly unchanged by [I18N-HDRS] (RFC6532).
|
1328
|
-
#
|
1329
|
-
# RFC5233:
|
1363
|
+
# See also RFC5233:
|
1330
1364
|
# optional-field = field-name ":" unstructured CRLF
|
1331
1365
|
# field-name = 1*ftext
|
1332
1366
|
# ftext = %d33-57 / ; Printable US-ASCII
|
1333
1367
|
# %d59-126 ; characters not including
|
1334
1368
|
# ; ":".
|
1335
|
-
|
1336
|
-
assert_no_lookahead
|
1337
|
-
start = @pos
|
1338
|
-
astring
|
1339
|
-
@str[start...@pos - 1]
|
1340
|
-
end
|
1369
|
+
alias header_fld_name astring
|
1341
1370
|
|
1342
1371
|
# mailbox-data = "FLAGS" SP flag-list / "LIST" SP mailbox-list /
|
1343
1372
|
# "LSUB" SP mailbox-list / "SEARCH" *(SP nz-number) /
|
@@ -1476,6 +1505,111 @@ module Net
|
|
1476
1505
|
end
|
1477
1506
|
alias sort_data mailbox_data__search
|
1478
1507
|
|
1508
|
+
# esearch-response = "ESEARCH" [search-correlator] [SP "UID"]
|
1509
|
+
# *(SP search-return-data)
|
1510
|
+
# ;; Note that SEARCH and ESEARCH responses
|
1511
|
+
# ;; SHOULD be mutually exclusive,
|
1512
|
+
# ;; i.e., only one of the response types
|
1513
|
+
# ;; should be
|
1514
|
+
# ;; returned as a result of a command.
|
1515
|
+
# esearch-response = "ESEARCH" [search-correlator] [SP "UID"]
|
1516
|
+
# *(SP search-return-data)
|
1517
|
+
# ; ESEARCH response replaces SEARCH response
|
1518
|
+
# ; from IMAP4rev1.
|
1519
|
+
# search-correlator = SP "(" "TAG" SP tag-string ")"
|
1520
|
+
def esearch_response
|
1521
|
+
name = label("ESEARCH")
|
1522
|
+
tag = search_correlator if peek_str?(" (")
|
1523
|
+
uid = peek_re?(/\G UID\b/i) && (SP!; label("UID"); true)
|
1524
|
+
data = []
|
1525
|
+
data << search_return_data while SP?
|
1526
|
+
esearch = ESearchResult.new(tag, uid, data)
|
1527
|
+
UntaggedResponse.new(name, esearch, @str)
|
1528
|
+
end
|
1529
|
+
|
1530
|
+
# From RFC4731 (ESEARCH):
|
1531
|
+
# search-return-data = "MIN" SP nz-number /
|
1532
|
+
# "MAX" SP nz-number /
|
1533
|
+
# "ALL" SP sequence-set /
|
1534
|
+
# "COUNT" SP number /
|
1535
|
+
# search-ret-data-ext
|
1536
|
+
# ; All return data items conform to
|
1537
|
+
# ; search-ret-data-ext syntax.
|
1538
|
+
# search-ret-data-ext = search-modifier-name SP search-return-value
|
1539
|
+
# search-modifier-name = tagged-ext-label
|
1540
|
+
# search-return-value = tagged-ext-val
|
1541
|
+
#
|
1542
|
+
# From RFC4731 (ESEARCH):
|
1543
|
+
# search-return-data =/ "MODSEQ" SP mod-sequence-value
|
1544
|
+
#
|
1545
|
+
# From RFC9394 (PARTIAL):
|
1546
|
+
# search-return-data =/ ret-data-partial
|
1547
|
+
#
|
1548
|
+
def search_return_data
|
1549
|
+
label = search_modifier_name; SP!
|
1550
|
+
value =
|
1551
|
+
case label
|
1552
|
+
when "MIN" then nz_number
|
1553
|
+
when "MAX" then nz_number
|
1554
|
+
when "ALL" then sequence_set
|
1555
|
+
when "COUNT" then number
|
1556
|
+
when "MODSEQ" then mod_sequence_value # RFC7162: CONDSTORE
|
1557
|
+
when "PARTIAL" then ret_data_partial__value # RFC9394: PARTIAL
|
1558
|
+
else search_return_value
|
1559
|
+
end
|
1560
|
+
[label, value]
|
1561
|
+
end
|
1562
|
+
|
1563
|
+
# From RFC5267 (CONTEXT=SEARCH, CONTEXT=SORT) and RFC9394 (PARTIAL):
|
1564
|
+
# ret-data-partial = "PARTIAL"
|
1565
|
+
# SP "(" partial-range SP partial-results ")"
|
1566
|
+
def ret_data_partial__value
|
1567
|
+
lpar
|
1568
|
+
range = partial_range; SP!
|
1569
|
+
results = partial_results
|
1570
|
+
rpar
|
1571
|
+
ESearchResult::PartialResult.new(range, results)
|
1572
|
+
end
|
1573
|
+
|
1574
|
+
# partial-range = partial-range-first / partial-range-last
|
1575
|
+
# tagged-ext-simple =/ partial-range-last
|
1576
|
+
def partial_range
|
1577
|
+
case (str = atom)
|
1578
|
+
when Patterns::PARTIAL_RANGE_FIRST, Patterns::PARTIAL_RANGE_LAST
|
1579
|
+
min, max = [Integer($1), Integer($2)].minmax
|
1580
|
+
min..max
|
1581
|
+
else
|
1582
|
+
parse_error("unexpected atom %p, expected partial-range", str)
|
1583
|
+
end
|
1584
|
+
end
|
1585
|
+
|
1586
|
+
# partial-results = sequence-set / "NIL"
|
1587
|
+
# ;; <sequence-set> from [RFC3501].
|
1588
|
+
# ;; NIL indicates that no results correspond to
|
1589
|
+
# ;; the requested range.
|
1590
|
+
def partial_results; NIL? ? nil : sequence_set end
|
1591
|
+
|
1592
|
+
# search-modifier-name = tagged-ext-label
|
1593
|
+
alias search_modifier_name tagged_ext_label
|
1594
|
+
|
1595
|
+
# search-return-value = tagged-ext-val
|
1596
|
+
# ; Data for the returned search option.
|
1597
|
+
# ; A single "nz-number"/"number"/"number64" value
|
1598
|
+
# ; can be returned as an atom (i.e., without
|
1599
|
+
# ; quoting). A sequence-set can be returned
|
1600
|
+
# ; as an atom as well.
|
1601
|
+
def search_return_value; ExtensionData.new(tagged_ext_val) end
|
1602
|
+
|
1603
|
+
# search-correlator = SP "(" "TAG" SP tag-string ")"
|
1604
|
+
def search_correlator
|
1605
|
+
SP!; lpar; label("TAG"); SP!; tag = tag_string; rpar
|
1606
|
+
tag
|
1607
|
+
end
|
1608
|
+
|
1609
|
+
# tag-string = astring
|
1610
|
+
# ; <tag> represented as <astring>
|
1611
|
+
alias tag_string astring
|
1612
|
+
|
1479
1613
|
# RFC5256: THREAD
|
1480
1614
|
# thread-data = "THREAD" [SP 1*thread-list]
|
1481
1615
|
def thread_data
|
@@ -1808,6 +1942,9 @@ module Net
|
|
1808
1942
|
#
|
1809
1943
|
# RFC8474: OBJECTID
|
1810
1944
|
# resp-text-code =/ "MAILBOXID" SP "(" objectid ")"
|
1945
|
+
#
|
1946
|
+
# RFC9586: UIDONLY
|
1947
|
+
# resp-text-code =/ "UIDREQUIRED"
|
1811
1948
|
def resp_text_code
|
1812
1949
|
name = resp_text_code__name
|
1813
1950
|
data =
|
@@ -1830,6 +1967,7 @@ module Net
|
|
1830
1967
|
when "HIGHESTMODSEQ" then SP!; mod_sequence_value # CONDSTORE
|
1831
1968
|
when "MODIFIED" then SP!; sequence_set # CONDSTORE
|
1832
1969
|
when "MAILBOXID" then SP!; parens__objectid # RFC8474: OBJECTID
|
1970
|
+
when "UIDREQUIRED" then # RFC9586: UIDONLY
|
1833
1971
|
else
|
1834
1972
|
SP? and text_chars_except_rbra
|
1835
1973
|
end
|
@@ -5,7 +5,7 @@ module Net
|
|
5
5
|
module SASL
|
6
6
|
|
7
7
|
# Authenticator for the "+ANONYMOUS+" SASL mechanism, as specified by
|
8
|
-
# RFC-4505[https://
|
8
|
+
# RFC-4505[https://www.rfc-editor.org/rfc/rfc4505]. See
|
9
9
|
# Net::IMAP#authenticate.
|
10
10
|
class AnonymousAuthenticator
|
11
11
|
|
@@ -13,7 +13,7 @@ module Net
|
|
13
13
|
# characters in length.
|
14
14
|
#
|
15
15
|
# If it contains an "@" sign, the message must be a valid email address
|
16
|
-
# (+addr-spec+ from RFC-2822[https://
|
16
|
+
# (+addr-spec+ from RFC-2822[https://www.rfc-editor.org/rfc/rfc2822]).
|
17
17
|
# Email syntax is _not_ validated by AnonymousAuthenticator.
|
18
18
|
#
|
19
19
|
# Otherwise, it can be any UTF8 string which is permitted by the
|
@@ -25,7 +25,7 @@ module Net
|
|
25
25
|
# new(anonymous_message: "", **) -> authenticator
|
26
26
|
#
|
27
27
|
# Creates an Authenticator for the "+ANONYMOUS+" SASL mechanism, as
|
28
|
-
# specified in RFC-4505[https://
|
28
|
+
# specified in RFC-4505[https://www.rfc-editor.org/rfc/rfc4505]. To use
|
29
29
|
# this, see Net::IMAP#authenticate or your client's authentication
|
30
30
|
# method.
|
31
31
|
#
|
@@ -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
|
@@ -1,16 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Authenticator for the "+CRAM-MD5+" SASL mechanism, specified in
|
4
|
-
# RFC2195[https://
|
4
|
+
# RFC2195[https://www.rfc-editor.org/rfc/rfc2195]. See Net::IMAP#authenticate.
|
5
5
|
#
|
6
6
|
# == Deprecated
|
7
7
|
#
|
8
8
|
# +CRAM-MD5+ is obsolete and insecure. It is included for compatibility with
|
9
9
|
# existing servers.
|
10
|
-
# {draft-ietf-sasl-crammd5-to-historic}[https://
|
10
|
+
# {draft-ietf-sasl-crammd5-to-historic}[https://www.rfc-editor.org/rfc/draft-ietf-sasl-crammd5-to-historic-00.html]
|
11
11
|
# recommends using +SCRAM-*+ or +PLAIN+ protected by TLS instead.
|
12
12
|
#
|
13
|
-
# Additionally, RFC8314[https://
|
13
|
+
# Additionally, RFC8314[https://www.rfc-editor.org/rfc/rfc8314] discourage the use
|
14
14
|
# of cleartext and recommends TLS version 1.2 or greater be used for all
|
15
15
|
# traffic. With TLS +CRAM-MD5+ is okay, but so is +PLAIN+
|
16
16
|
class Net::IMAP::SASL::CramMD5Authenticator
|
@@ -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
|