net-imap 0.4.22 → 0.6.3
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/Gemfile +12 -2
- 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 +40 -95
- data/lib/net/imap/config/attr_accessors.rb +8 -9
- data/lib/net/imap/config/attr_inheritance.rb +64 -1
- data/lib/net/imap/config/attr_type_coercion.rb +22 -10
- data/lib/net/imap/config/attr_version_defaults.rb +90 -0
- data/lib/net/imap/config.rb +241 -125
- data/lib/net/imap/connection_state.rb +48 -0
- data/lib/net/imap/data_encoding.rb +80 -31
- data/lib/net/imap/deprecated_client_options.rb +6 -3
- data/lib/net/imap/errors.rb +158 -0
- data/lib/net/imap/esearch_result.rb +225 -0
- data/lib/net/imap/fetch_data.rb +126 -47
- data/lib/net/imap/flags.rb +1 -1
- data/lib/net/imap/response_data.rb +123 -187
- data/lib/net/imap/response_parser/parser_utils.rb +19 -23
- data/lib/net/imap/response_parser.rb +182 -38
- data/lib/net/imap/response_reader.rb +10 -12
- 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 +10 -5
- data/lib/net/imap/sequence_set.rb +1104 -421
- data/lib/net/imap/stringprep/nameprep.rb +1 -1
- data/lib/net/imap/stringprep/trace.rb +4 -4
- data/lib/net/imap/uidplus_data.rb +4 -147
- data/lib/net/imap/vanished_data.rb +65 -0
- data/lib/net/imap.rb +1002 -313
- data/net-imap.gemspec +1 -1
- data/rakelib/rfcs.rake +2 -0
- data/rakelib/string_prep_tables_generator.rb +6 -2
- metadata +7 -3
|
@@ -185,6 +185,11 @@ module Net
|
|
|
185
185
|
@str[@pos, str.length] == str
|
|
186
186
|
end
|
|
187
187
|
|
|
188
|
+
def peek_re?(re)
|
|
189
|
+
assert_no_lookahead if Net::IMAP.debug
|
|
190
|
+
re.match?(@str, @pos)
|
|
191
|
+
end
|
|
192
|
+
|
|
188
193
|
def peek_re(re)
|
|
189
194
|
assert_no_lookahead if config.debug?
|
|
190
195
|
re.match(@str, @pos)
|
|
@@ -210,29 +215,20 @@ module Net
|
|
|
210
215
|
@token = nil
|
|
211
216
|
end
|
|
212
217
|
|
|
213
|
-
def parse_error(fmt, *args)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
idx,
|
|
228
|
-
cloc.base_label,
|
|
229
|
-
File.basename(cloc.path, ".rb"),
|
|
230
|
-
cloc.lineno
|
|
231
|
-
]
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
raise ResponseParseError, msg
|
|
235
|
-
end
|
|
218
|
+
def parse_error(fmt, *args) = raise exception format(fmt, *args)
|
|
219
|
+
|
|
220
|
+
def exception(message) = ResponseParseError.new(
|
|
221
|
+
message, parser_state:, parser_class: self.class
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# This can be used to backtrack after a parse error, and re-attempt to
|
|
225
|
+
# parse using a fallback.
|
|
226
|
+
#
|
|
227
|
+
# NOTE: Reckless backtracking could lead to O(n²) situations, so this
|
|
228
|
+
# should very rarely be used. Ideally, fallbacks should not backtrack.
|
|
229
|
+
def restore_state(state) = (@lex_state, @pos, @token = state)
|
|
230
|
+
def current_state = [@lex_state, @pos, @token]
|
|
231
|
+
def parser_state = [@str, *current_state]
|
|
236
232
|
|
|
237
233
|
end
|
|
238
234
|
end
|
|
@@ -38,6 +38,11 @@ module Net
|
|
|
38
38
|
@lex_state = EXPR_BEG
|
|
39
39
|
@token = nil
|
|
40
40
|
return response
|
|
41
|
+
rescue ResponseParseError => error
|
|
42
|
+
if config.debug?
|
|
43
|
+
warn error.detailed_message(parser_state: true, parser_backtrace: true)
|
|
44
|
+
end
|
|
45
|
+
raise
|
|
41
46
|
end
|
|
42
47
|
|
|
43
48
|
private
|
|
@@ -325,6 +330,24 @@ module Net
|
|
|
325
330
|
SEQUENCE_SET = /#{SEQUENCE_SET_ITEM}(?:,#{SEQUENCE_SET_ITEM})*/n
|
|
326
331
|
SEQUENCE_SET_STR = /\A#{SEQUENCE_SET}\z/n
|
|
327
332
|
|
|
333
|
+
# partial-range-first = nz-number ":" nz-number
|
|
334
|
+
# ;; Request to search from oldest (lowest UIDs) to
|
|
335
|
+
# ;; more recent messages.
|
|
336
|
+
# ;; A range 500:400 is the same as 400:500.
|
|
337
|
+
# ;; This is similar to <seq-range> from [RFC3501]
|
|
338
|
+
# ;; but cannot contain "*".
|
|
339
|
+
PARTIAL_RANGE_FIRST = /\A(#{NZ_NUMBER}):(#{NZ_NUMBER})\z/n
|
|
340
|
+
|
|
341
|
+
# partial-range-last = MINUS nz-number ":" MINUS nz-number
|
|
342
|
+
# ;; Request to search from newest (highest UIDs) to
|
|
343
|
+
# ;; oldest messages.
|
|
344
|
+
# ;; A range -500:-400 is the same as -400:-500.
|
|
345
|
+
PARTIAL_RANGE_LAST = /\A(-#{NZ_NUMBER}):(-#{NZ_NUMBER})\z/n
|
|
346
|
+
|
|
347
|
+
# partial-range = partial-range-first / partial-range-last
|
|
348
|
+
PARTIAL_RANGE = Regexp.union(PARTIAL_RANGE_FIRST,
|
|
349
|
+
PARTIAL_RANGE_LAST)
|
|
350
|
+
|
|
328
351
|
# RFC3501:
|
|
329
352
|
# literal = "{" number "}" CRLF *CHAR8
|
|
330
353
|
# ; Number represents the number of CHAR8s
|
|
@@ -720,7 +743,7 @@ module Net
|
|
|
720
743
|
when "EXISTS" then mailbox_data__exists # RFC3501, RFC9051
|
|
721
744
|
when "ESEARCH" then esearch_response # RFC4731, RFC9051, etc
|
|
722
745
|
when "VANISHED" then expunged_resp # RFC7162
|
|
723
|
-
when "UIDFETCH" then uidfetch_resp #
|
|
746
|
+
when "UIDFETCH" then uidfetch_resp # RFC9586
|
|
724
747
|
when "SEARCH" then mailbox_data__search # RFC3501 (obsolete)
|
|
725
748
|
when "CAPABILITY" then capability_data__untagged # RFC3501, RFC9051
|
|
726
749
|
when "FLAGS" then mailbox_data__flags # RFC3501, RFC9051
|
|
@@ -773,9 +796,6 @@ module Net
|
|
|
773
796
|
def response_data__ignored; response_data__unhandled(IgnoredResponse) end
|
|
774
797
|
alias response_data__noop response_data__ignored
|
|
775
798
|
|
|
776
|
-
alias esearch_response response_data__unhandled
|
|
777
|
-
alias expunged_resp response_data__unhandled
|
|
778
|
-
alias uidfetch_resp response_data__unhandled
|
|
779
799
|
alias listrights_data response_data__unhandled
|
|
780
800
|
alias myrights_data response_data__unhandled
|
|
781
801
|
alias metadata_resp response_data__unhandled
|
|
@@ -836,6 +856,14 @@ module Net
|
|
|
836
856
|
UntaggedResponse.new(name, data, @str)
|
|
837
857
|
end
|
|
838
858
|
|
|
859
|
+
# uidfetch-resp = uniqueid SP "UIDFETCH" SP msg-att
|
|
860
|
+
def uidfetch_resp
|
|
861
|
+
uid = uniqueid; SP!
|
|
862
|
+
name = label "UIDFETCH"; SP!
|
|
863
|
+
data = UIDFetchData.new(uid, msg_att(uid))
|
|
864
|
+
UntaggedResponse.new(name, data, @str)
|
|
865
|
+
end
|
|
866
|
+
|
|
839
867
|
def response_data__simple_numeric
|
|
840
868
|
data = nz_number; SP!
|
|
841
869
|
name = tagged_ext_label
|
|
@@ -846,6 +874,20 @@ module Net
|
|
|
846
874
|
alias mailbox_data__exists response_data__simple_numeric
|
|
847
875
|
alias mailbox_data__recent response_data__simple_numeric
|
|
848
876
|
|
|
877
|
+
# The name for this is confusing, because it *replaces* EXPUNGE
|
|
878
|
+
# >>>
|
|
879
|
+
# expunged-resp = "VANISHED" [SP "(EARLIER)"] SP known-uids
|
|
880
|
+
def expunged_resp
|
|
881
|
+
name = label "VANISHED"; SP!
|
|
882
|
+
earlier = if lpar? then label("EARLIER"); rpar; SP!; true else false end
|
|
883
|
+
uids = known_uids
|
|
884
|
+
data = VanishedData[uids, earlier]
|
|
885
|
+
UntaggedResponse.new name, data, @str
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
# TODO: replace with uid_set
|
|
889
|
+
alias known_uids sequence_set
|
|
890
|
+
|
|
849
891
|
# RFC3501 & RFC9051:
|
|
850
892
|
# msg-att = "(" (msg-att-dynamic / msg-att-static)
|
|
851
893
|
# *(SP (msg-att-dynamic / msg-att-static)) ")"
|
|
@@ -1321,31 +1363,19 @@ module Net
|
|
|
1321
1363
|
# header-fld-name = astring
|
|
1322
1364
|
#
|
|
1323
1365
|
# NOTE: Previously, Net::IMAP recreated the raw original source string.
|
|
1324
|
-
# Now, it
|
|
1325
|
-
#
|
|
1326
|
-
#
|
|
1327
|
-
# standard header field names are valid atoms:
|
|
1366
|
+
# Now, it returns the decoded astring value. Although this is technically
|
|
1367
|
+
# incompatible, it should almost never make a difference: all standard
|
|
1368
|
+
# header field names are valid atoms:
|
|
1328
1369
|
#
|
|
1329
1370
|
# https://www.iana.org/assignments/message-headers/message-headers.xhtml
|
|
1330
1371
|
#
|
|
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:
|
|
1372
|
+
# See also RFC5233:
|
|
1337
1373
|
# optional-field = field-name ":" unstructured CRLF
|
|
1338
1374
|
# field-name = 1*ftext
|
|
1339
1375
|
# ftext = %d33-57 / ; Printable US-ASCII
|
|
1340
1376
|
# %d59-126 ; characters not including
|
|
1341
1377
|
# ; ":".
|
|
1342
|
-
|
|
1343
|
-
assert_no_lookahead
|
|
1344
|
-
start = @pos
|
|
1345
|
-
astring
|
|
1346
|
-
end_pos = @token ? @pos - 1 : @pos
|
|
1347
|
-
@str[start...end_pos]
|
|
1348
|
-
end
|
|
1378
|
+
alias header_fld_name astring
|
|
1349
1379
|
|
|
1350
1380
|
# mailbox-data = "FLAGS" SP flag-list / "LIST" SP mailbox-list /
|
|
1351
1381
|
# "LSUB" SP mailbox-list / "SEARCH" *(SP nz-number) /
|
|
@@ -1484,6 +1514,111 @@ module Net
|
|
|
1484
1514
|
end
|
|
1485
1515
|
alias sort_data mailbox_data__search
|
|
1486
1516
|
|
|
1517
|
+
# esearch-response = "ESEARCH" [search-correlator] [SP "UID"]
|
|
1518
|
+
# *(SP search-return-data)
|
|
1519
|
+
# ;; Note that SEARCH and ESEARCH responses
|
|
1520
|
+
# ;; SHOULD be mutually exclusive,
|
|
1521
|
+
# ;; i.e., only one of the response types
|
|
1522
|
+
# ;; should be
|
|
1523
|
+
# ;; returned as a result of a command.
|
|
1524
|
+
# esearch-response = "ESEARCH" [search-correlator] [SP "UID"]
|
|
1525
|
+
# *(SP search-return-data)
|
|
1526
|
+
# ; ESEARCH response replaces SEARCH response
|
|
1527
|
+
# ; from IMAP4rev1.
|
|
1528
|
+
# search-correlator = SP "(" "TAG" SP tag-string ")"
|
|
1529
|
+
def esearch_response
|
|
1530
|
+
name = label("ESEARCH")
|
|
1531
|
+
tag = search_correlator if peek_str?(" (")
|
|
1532
|
+
uid = peek_re?(/\G UID\b/i) && (SP!; label("UID"); true)
|
|
1533
|
+
data = []
|
|
1534
|
+
data << search_return_data while SP?
|
|
1535
|
+
esearch = ESearchResult.new(tag, uid, data)
|
|
1536
|
+
UntaggedResponse.new(name, esearch, @str)
|
|
1537
|
+
end
|
|
1538
|
+
|
|
1539
|
+
# From RFC4731 (ESEARCH):
|
|
1540
|
+
# search-return-data = "MIN" SP nz-number /
|
|
1541
|
+
# "MAX" SP nz-number /
|
|
1542
|
+
# "ALL" SP sequence-set /
|
|
1543
|
+
# "COUNT" SP number /
|
|
1544
|
+
# search-ret-data-ext
|
|
1545
|
+
# ; All return data items conform to
|
|
1546
|
+
# ; search-ret-data-ext syntax.
|
|
1547
|
+
# search-ret-data-ext = search-modifier-name SP search-return-value
|
|
1548
|
+
# search-modifier-name = tagged-ext-label
|
|
1549
|
+
# search-return-value = tagged-ext-val
|
|
1550
|
+
#
|
|
1551
|
+
# From RFC4731 (ESEARCH):
|
|
1552
|
+
# search-return-data =/ "MODSEQ" SP mod-sequence-value
|
|
1553
|
+
#
|
|
1554
|
+
# From RFC9394 (PARTIAL):
|
|
1555
|
+
# search-return-data =/ ret-data-partial
|
|
1556
|
+
#
|
|
1557
|
+
def search_return_data
|
|
1558
|
+
label = search_modifier_name; SP!
|
|
1559
|
+
value =
|
|
1560
|
+
case label
|
|
1561
|
+
when "MIN" then nz_number
|
|
1562
|
+
when "MAX" then nz_number
|
|
1563
|
+
when "ALL" then sequence_set
|
|
1564
|
+
when "COUNT" then number
|
|
1565
|
+
when "MODSEQ" then mod_sequence_value # RFC7162: CONDSTORE
|
|
1566
|
+
when "PARTIAL" then ret_data_partial__value # RFC9394: PARTIAL
|
|
1567
|
+
else search_return_value
|
|
1568
|
+
end
|
|
1569
|
+
[label, value]
|
|
1570
|
+
end
|
|
1571
|
+
|
|
1572
|
+
# From RFC5267 (CONTEXT=SEARCH, CONTEXT=SORT) and RFC9394 (PARTIAL):
|
|
1573
|
+
# ret-data-partial = "PARTIAL"
|
|
1574
|
+
# SP "(" partial-range SP partial-results ")"
|
|
1575
|
+
def ret_data_partial__value
|
|
1576
|
+
lpar
|
|
1577
|
+
range = partial_range; SP!
|
|
1578
|
+
results = partial_results
|
|
1579
|
+
rpar
|
|
1580
|
+
ESearchResult::PartialResult.new(range, results)
|
|
1581
|
+
end
|
|
1582
|
+
|
|
1583
|
+
# partial-range = partial-range-first / partial-range-last
|
|
1584
|
+
# tagged-ext-simple =/ partial-range-last
|
|
1585
|
+
def partial_range
|
|
1586
|
+
case (str = atom)
|
|
1587
|
+
when Patterns::PARTIAL_RANGE_FIRST, Patterns::PARTIAL_RANGE_LAST
|
|
1588
|
+
min, max = [Integer($1), Integer($2)].minmax
|
|
1589
|
+
min..max
|
|
1590
|
+
else
|
|
1591
|
+
parse_error("unexpected atom %p, expected partial-range", str)
|
|
1592
|
+
end
|
|
1593
|
+
end
|
|
1594
|
+
|
|
1595
|
+
# partial-results = sequence-set / "NIL"
|
|
1596
|
+
# ;; <sequence-set> from [RFC3501].
|
|
1597
|
+
# ;; NIL indicates that no results correspond to
|
|
1598
|
+
# ;; the requested range.
|
|
1599
|
+
def partial_results; NIL? ? nil : sequence_set end
|
|
1600
|
+
|
|
1601
|
+
# search-modifier-name = tagged-ext-label
|
|
1602
|
+
alias search_modifier_name tagged_ext_label
|
|
1603
|
+
|
|
1604
|
+
# search-return-value = tagged-ext-val
|
|
1605
|
+
# ; Data for the returned search option.
|
|
1606
|
+
# ; A single "nz-number"/"number"/"number64" value
|
|
1607
|
+
# ; can be returned as an atom (i.e., without
|
|
1608
|
+
# ; quoting). A sequence-set can be returned
|
|
1609
|
+
# ; as an atom as well.
|
|
1610
|
+
def search_return_value; ExtensionData.new(tagged_ext_val) end
|
|
1611
|
+
|
|
1612
|
+
# search-correlator = SP "(" "TAG" SP tag-string ")"
|
|
1613
|
+
def search_correlator
|
|
1614
|
+
SP!; lpar; label("TAG"); SP!; tag = tag_string; rpar
|
|
1615
|
+
tag
|
|
1616
|
+
end
|
|
1617
|
+
|
|
1618
|
+
# tag-string = astring
|
|
1619
|
+
# ; <tag> represented as <astring>
|
|
1620
|
+
alias tag_string astring
|
|
1621
|
+
|
|
1487
1622
|
# RFC5256: THREAD
|
|
1488
1623
|
# thread-data = "THREAD" [SP 1*thread-list]
|
|
1489
1624
|
def thread_data
|
|
@@ -1753,12 +1888,17 @@ module Net
|
|
|
1753
1888
|
# We leniently re-interpret this as
|
|
1754
1889
|
# resp-text = ["[" resp-text-code "]" [SP [text]] / [text]
|
|
1755
1890
|
def resp_text
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1891
|
+
begin
|
|
1892
|
+
state = current_state
|
|
1893
|
+
if lbra?
|
|
1894
|
+
code = resp_text_code; rbra
|
|
1895
|
+
return ResponseText.new(code, SP? && text? || "")
|
|
1896
|
+
end
|
|
1897
|
+
rescue ResponseParseError => error
|
|
1898
|
+
raise if /\buid-set\b/i.match? error.message
|
|
1899
|
+
restore_state state
|
|
1761
1900
|
end
|
|
1901
|
+
ResponseText.new(nil, text? || "")
|
|
1762
1902
|
end
|
|
1763
1903
|
|
|
1764
1904
|
# RFC3501 (See https://www.rfc-editor.org/errata/rfc3501):
|
|
@@ -1816,6 +1956,9 @@ module Net
|
|
|
1816
1956
|
#
|
|
1817
1957
|
# RFC8474: OBJECTID
|
|
1818
1958
|
# resp-text-code =/ "MAILBOXID" SP "(" objectid ")"
|
|
1959
|
+
#
|
|
1960
|
+
# RFC9586: UIDONLY
|
|
1961
|
+
# resp-text-code =/ "UIDREQUIRED"
|
|
1819
1962
|
def resp_text_code
|
|
1820
1963
|
name = resp_text_code__name
|
|
1821
1964
|
data =
|
|
@@ -1838,6 +1981,7 @@ module Net
|
|
|
1838
1981
|
when "HIGHESTMODSEQ" then SP!; mod_sequence_value # CONDSTORE
|
|
1839
1982
|
when "MODIFIED" then SP!; sequence_set # CONDSTORE
|
|
1840
1983
|
when "MAILBOXID" then SP!; parens__objectid # RFC8474: OBJECTID
|
|
1984
|
+
when "UIDREQUIRED" then # RFC9586: UIDONLY
|
|
1841
1985
|
else
|
|
1842
1986
|
SP? and text_chars_except_rbra
|
|
1843
1987
|
end
|
|
@@ -1883,24 +2027,24 @@ module Net
|
|
|
1883
2027
|
CopyUID(validity, src_uids, dst_uids)
|
|
1884
2028
|
end
|
|
1885
2029
|
|
|
1886
|
-
|
|
1887
|
-
def CopyUID(...) DeprecatedUIDPlus(...) || CopyUIDData.new(...) end
|
|
2030
|
+
PARSER_PATH = File.expand_path(__FILE__).delete_suffix(".rb")
|
|
1888
2031
|
|
|
1889
2032
|
# TODO: remove this code in the v0.6.0 release
|
|
1890
2033
|
def DeprecatedUIDPlus(validity, src_uids = nil, dst_uids)
|
|
1891
2034
|
return unless config.parser_use_deprecated_uidplus_data
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
parse_error("uid-set is too large: %d > %d", count, max)
|
|
1901
|
-
end
|
|
2035
|
+
uplevel = caller_locations
|
|
2036
|
+
.find_index { !_1.path.start_with?(PARSER_PATH) }
|
|
2037
|
+
&.succ
|
|
2038
|
+
warn("#{Config}#parser_use_deprecated_uidplus_data is ignored " \
|
|
2039
|
+
"since v0.6.0. Disable this warning by setting " \
|
|
2040
|
+
"config.parser_use_deprecated_uidplus_data = false.",
|
|
2041
|
+
category: :deprecated, uplevel:)
|
|
2042
|
+
nil
|
|
1902
2043
|
end
|
|
1903
2044
|
|
|
2045
|
+
def AppendUID(...) DeprecatedUIDPlus(...) || AppendUIDData.new(...) end
|
|
2046
|
+
def CopyUID(...) DeprecatedUIDPlus(...) || CopyUIDData.new(...) end
|
|
2047
|
+
|
|
1904
2048
|
ADDRESS_REGEXP = /\G
|
|
1905
2049
|
\( (?: NIL | #{Patterns::QUOTED_rev2} ) # 1: NAME
|
|
1906
2050
|
\s (?: NIL | #{Patterns::QUOTED_rev2} ) # 2: ROUTE
|
|
@@ -28,11 +28,11 @@ module Net
|
|
|
28
28
|
|
|
29
29
|
attr_reader :buff, :literal_size
|
|
30
30
|
|
|
31
|
-
def bytes_read
|
|
32
|
-
def empty
|
|
33
|
-
def done
|
|
34
|
-
def line_done
|
|
35
|
-
def get_literal_size
|
|
31
|
+
def bytes_read = buff.bytesize
|
|
32
|
+
def empty? = buff.empty?
|
|
33
|
+
def done? = line_done? && !get_literal_size
|
|
34
|
+
def line_done? = buff.end_with?(CRLF)
|
|
35
|
+
def get_literal_size = /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i
|
|
36
36
|
|
|
37
37
|
def read_line
|
|
38
38
|
buff << (@sock.gets(CRLF, read_limit) or throw :eof)
|
|
@@ -52,10 +52,10 @@ module Net
|
|
|
52
52
|
[limit, max_response_remaining!].compact.min
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
def max_response_size
|
|
56
|
-
def max_response_remaining
|
|
57
|
-
def response_too_large
|
|
58
|
-
def min_response_size
|
|
55
|
+
def max_response_size = client.max_response_size
|
|
56
|
+
def max_response_remaining = max_response_size &.- bytes_read
|
|
57
|
+
def response_too_large? = max_response_size &.< min_response_size
|
|
58
|
+
def min_response_size = bytes_read + min_response_remaining
|
|
59
59
|
|
|
60
60
|
def min_response_remaining
|
|
61
61
|
empty? ? 3 : done? ? 0 : (literal_size || 0) + 2
|
|
@@ -64,9 +64,7 @@ module Net
|
|
|
64
64
|
def max_response_remaining!
|
|
65
65
|
return max_response_remaining unless response_too_large?
|
|
66
66
|
raise ResponseTooLargeError.new(
|
|
67
|
-
max_response_size
|
|
68
|
-
bytes_read: bytes_read,
|
|
69
|
-
literal_size: literal_size,
|
|
67
|
+
max_response_size:, bytes_read:, literal_size:,
|
|
70
68
|
)
|
|
71
69
|
end
|
|
72
70
|
|
|
@@ -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
|