net-imap 0.4.12 → 0.5.5
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/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
|