net-imap 0.4.9.1 → 0.5.6

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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/BSDL +22 -0
  3. data/COPYING +56 -0
  4. data/Gemfile +12 -1
  5. data/LICENSE.txt +3 -22
  6. data/README.md +10 -4
  7. data/docs/styles.css +75 -14
  8. data/lib/net/imap/authenticators.rb +2 -2
  9. data/lib/net/imap/command_data.rb +61 -48
  10. data/lib/net/imap/config/attr_accessors.rb +75 -0
  11. data/lib/net/imap/config/attr_inheritance.rb +90 -0
  12. data/lib/net/imap/config/attr_type_coercion.rb +61 -0
  13. data/lib/net/imap/config.rb +470 -0
  14. data/lib/net/imap/data_encoding.rb +4 -4
  15. data/lib/net/imap/data_lite.rb +226 -0
  16. data/lib/net/imap/deprecated_client_options.rb +9 -6
  17. data/lib/net/imap/errors.rb +7 -1
  18. data/lib/net/imap/esearch_result.rb +180 -0
  19. data/lib/net/imap/fetch_data.rb +126 -47
  20. data/lib/net/imap/flags.rb +1 -1
  21. data/lib/net/imap/response_data.rb +126 -239
  22. data/lib/net/imap/response_parser/parser_utils.rb +11 -6
  23. data/lib/net/imap/response_parser.rb +188 -34
  24. data/lib/net/imap/sasl/anonymous_authenticator.rb +3 -3
  25. data/lib/net/imap/sasl/authentication_exchange.rb +52 -20
  26. data/lib/net/imap/sasl/authenticators.rb +8 -4
  27. data/lib/net/imap/sasl/client_adapter.rb +77 -26
  28. data/lib/net/imap/sasl/cram_md5_authenticator.rb +4 -4
  29. data/lib/net/imap/sasl/digest_md5_authenticator.rb +218 -56
  30. data/lib/net/imap/sasl/external_authenticator.rb +3 -3
  31. data/lib/net/imap/sasl/gs2_header.rb +7 -7
  32. data/lib/net/imap/sasl/login_authenticator.rb +4 -3
  33. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +6 -6
  34. data/lib/net/imap/sasl/plain_authenticator.rb +7 -7
  35. data/lib/net/imap/sasl/protocol_adapters.rb +60 -4
  36. data/lib/net/imap/sasl/scram_authenticator.rb +8 -8
  37. data/lib/net/imap/sasl.rb +8 -5
  38. data/lib/net/imap/sasl_adapter.rb +0 -1
  39. data/lib/net/imap/search_result.rb +4 -8
  40. data/lib/net/imap/sequence_set.rb +239 -88
  41. data/lib/net/imap/stringprep/nameprep.rb +1 -1
  42. data/lib/net/imap/stringprep/trace.rb +4 -4
  43. data/lib/net/imap/uidplus_data.rb +244 -0
  44. data/lib/net/imap/vanished_data.rb +56 -0
  45. data/lib/net/imap.rb +1012 -322
  46. data/net-imap.gemspec +4 -7
  47. data/rakelib/benchmarks.rake +1 -1
  48. data/rakelib/rfcs.rake +2 -0
  49. data/rakelib/string_prep_tables_generator.rb +2 -0
  50. data/sample/net-imap.rb +167 -0
  51. metadata +16 -40
  52. data/.github/dependabot.yml +0 -6
  53. data/.github/workflows/pages.yml +0 -46
  54. data/.github/workflows/test.yml +0 -31
  55. data/.gitignore +0 -12
@@ -154,7 +154,7 @@ module Net
154
154
  end
155
155
 
156
156
  # To be used conditionally:
157
- # assert_no_lookahead if Net::IMAP.debug
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 Net::IMAP.debug
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 Net::IMAP.debug
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 Net::IMAP.debug
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 IMAP.debug
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,19 @@ module Net
11
11
  include ParserUtils
12
12
  extend ParserUtils::Generator
13
13
 
14
- # :call-seq: Net::IMAP::ResponseParser.new -> Net::IMAP::ResponseParser
15
- def initialize
14
+ attr_reader :config
15
+
16
+ # Creates a new ResponseParser.
17
+ #
18
+ # When +config+ is frozen or global, the parser #config inherits from it.
19
+ # Otherwise, +config+ will be used directly.
20
+ def initialize(config: Config.global)
16
21
  @str = nil
17
22
  @pos = nil
18
23
  @lex_state = nil
19
24
  @token = nil
25
+ @config = Config[config]
26
+ @config = @config.new if @config == Config.global || @config.frozen?
20
27
  end
21
28
 
22
29
  # :call-seq:
@@ -318,6 +325,24 @@ module Net
318
325
  SEQUENCE_SET = /#{SEQUENCE_SET_ITEM}(?:,#{SEQUENCE_SET_ITEM})*/n
319
326
  SEQUENCE_SET_STR = /\A#{SEQUENCE_SET}\z/n
320
327
 
328
+ # partial-range-first = nz-number ":" nz-number
329
+ # ;; Request to search from oldest (lowest UIDs) to
330
+ # ;; more recent messages.
331
+ # ;; A range 500:400 is the same as 400:500.
332
+ # ;; This is similar to <seq-range> from [RFC3501]
333
+ # ;; but cannot contain "*".
334
+ PARTIAL_RANGE_FIRST = /\A(#{NZ_NUMBER}):(#{NZ_NUMBER})\z/n
335
+
336
+ # partial-range-last = MINUS nz-number ":" MINUS nz-number
337
+ # ;; Request to search from newest (highest UIDs) to
338
+ # ;; oldest messages.
339
+ # ;; A range -500:-400 is the same as -400:-500.
340
+ PARTIAL_RANGE_LAST = /\A(-#{NZ_NUMBER}):(-#{NZ_NUMBER})\z/n
341
+
342
+ # partial-range = partial-range-first / partial-range-last
343
+ PARTIAL_RANGE = Regexp.union(PARTIAL_RANGE_FIRST,
344
+ PARTIAL_RANGE_LAST)
345
+
321
346
  # RFC3501:
322
347
  # literal = "{" number "}" CRLF *CHAR8
323
348
  # ; Number represents the number of CHAR8s
@@ -713,7 +738,7 @@ module Net
713
738
  when "EXISTS" then mailbox_data__exists # RFC3501, RFC9051
714
739
  when "ESEARCH" then esearch_response # RFC4731, RFC9051, etc
715
740
  when "VANISHED" then expunged_resp # RFC7162
716
- when "UIDFETCH" then uidfetch_resp # (draft) UIDONLY
741
+ when "UIDFETCH" then uidfetch_resp # RFC9586
717
742
  when "SEARCH" then mailbox_data__search # RFC3501 (obsolete)
718
743
  when "CAPABILITY" then capability_data__untagged # RFC3501, RFC9051
719
744
  when "FLAGS" then mailbox_data__flags # RFC3501, RFC9051
@@ -766,9 +791,6 @@ module Net
766
791
  def response_data__ignored; response_data__unhandled(IgnoredResponse) end
767
792
  alias response_data__noop response_data__ignored
768
793
 
769
- alias esearch_response response_data__unhandled
770
- alias expunged_resp response_data__unhandled
771
- alias uidfetch_resp response_data__unhandled
772
794
  alias listrights_data response_data__unhandled
773
795
  alias myrights_data response_data__unhandled
774
796
  alias metadata_resp response_data__unhandled
@@ -829,6 +851,14 @@ module Net
829
851
  UntaggedResponse.new(name, data, @str)
830
852
  end
831
853
 
854
+ # uidfetch-resp = uniqueid SP "UIDFETCH" SP msg-att
855
+ def uidfetch_resp
856
+ uid = uniqueid; SP!
857
+ name = label "UIDFETCH"; SP!
858
+ data = UIDFetchData.new(uid, msg_att(uid))
859
+ UntaggedResponse.new(name, data, @str)
860
+ end
861
+
832
862
  def response_data__simple_numeric
833
863
  data = nz_number; SP!
834
864
  name = tagged_ext_label
@@ -839,6 +869,20 @@ module Net
839
869
  alias mailbox_data__exists response_data__simple_numeric
840
870
  alias mailbox_data__recent response_data__simple_numeric
841
871
 
872
+ # The name for this is confusing, because it *replaces* EXPUNGE
873
+ # >>>
874
+ # expunged-resp = "VANISHED" [SP "(EARLIER)"] SP known-uids
875
+ def expunged_resp
876
+ name = label "VANISHED"; SP!
877
+ earlier = if lpar? then label("EARLIER"); rpar; SP!; true else false end
878
+ uids = known_uids
879
+ data = VanishedData[uids, earlier]
880
+ UntaggedResponse.new name, data, @str
881
+ end
882
+
883
+ # TODO: replace with uid_set
884
+ alias known_uids sequence_set
885
+
842
886
  # RFC3501 & RFC9051:
843
887
  # msg-att = "(" (msg-att-dynamic / msg-att-static)
844
888
  # *(SP (msg-att-dynamic / msg-att-static)) ")"
@@ -1155,6 +1199,7 @@ module Net
1155
1199
  # RFC3501, RFC9051:
1156
1200
  # body-fld-param = "(" string SP string *(SP string SP string) ")" / nil
1157
1201
  def body_fld_param
1202
+ quirky_SP? # See comments on test_bodystructure_extra_space
1158
1203
  return if NIL?
1159
1204
  param = {}
1160
1205
  lpar
@@ -1313,30 +1358,19 @@ module Net
1313
1358
  # header-fld-name = astring
1314
1359
  #
1315
1360
  # NOTE: Previously, Net::IMAP recreated the raw original source string.
1316
- # Now, it grabs the raw encoded value using @str and @pos. A future
1317
- # version may simply return the decoded astring value. Although that is
1318
- # technically incompatible, it should almost never make a difference: all
1319
- # standard header field names are valid atoms:
1361
+ # Now, it returns the decoded astring value. Although this is technically
1362
+ # incompatible, it should almost never make a difference: all standard
1363
+ # header field names are valid atoms:
1320
1364
  #
1321
1365
  # https://www.iana.org/assignments/message-headers/message-headers.xhtml
1322
1366
  #
1323
- # Although RFC3501 allows any astring, RFC5322-valid header names are one
1324
- # or more of the printable US-ASCII characters, except SP and colon. So
1325
- # empty string isn't valid, and literals aren't needed and should not be
1326
- # used. This is explicitly unchanged by [I18N-HDRS] (RFC6532).
1327
- #
1328
- # RFC5233:
1367
+ # See also RFC5233:
1329
1368
  # optional-field = field-name ":" unstructured CRLF
1330
1369
  # field-name = 1*ftext
1331
1370
  # ftext = %d33-57 / ; Printable US-ASCII
1332
1371
  # %d59-126 ; characters not including
1333
1372
  # ; ":".
1334
- def header_fld_name
1335
- assert_no_lookahead
1336
- start = @pos
1337
- astring
1338
- @str[start...@pos - 1]
1339
- end
1373
+ alias header_fld_name astring
1340
1374
 
1341
1375
  # mailbox-data = "FLAGS" SP flag-list / "LIST" SP mailbox-list /
1342
1376
  # "LSUB" SP mailbox-list / "SEARCH" *(SP nz-number) /
@@ -1475,6 +1509,111 @@ module Net
1475
1509
  end
1476
1510
  alias sort_data mailbox_data__search
1477
1511
 
1512
+ # esearch-response = "ESEARCH" [search-correlator] [SP "UID"]
1513
+ # *(SP search-return-data)
1514
+ # ;; Note that SEARCH and ESEARCH responses
1515
+ # ;; SHOULD be mutually exclusive,
1516
+ # ;; i.e., only one of the response types
1517
+ # ;; should be
1518
+ # ;; returned as a result of a command.
1519
+ # esearch-response = "ESEARCH" [search-correlator] [SP "UID"]
1520
+ # *(SP search-return-data)
1521
+ # ; ESEARCH response replaces SEARCH response
1522
+ # ; from IMAP4rev1.
1523
+ # search-correlator = SP "(" "TAG" SP tag-string ")"
1524
+ def esearch_response
1525
+ name = label("ESEARCH")
1526
+ tag = search_correlator if peek_str?(" (")
1527
+ uid = peek_re?(/\G UID\b/i) && (SP!; label("UID"); true)
1528
+ data = []
1529
+ data << search_return_data while SP?
1530
+ esearch = ESearchResult.new(tag, uid, data)
1531
+ UntaggedResponse.new(name, esearch, @str)
1532
+ end
1533
+
1534
+ # From RFC4731 (ESEARCH):
1535
+ # search-return-data = "MIN" SP nz-number /
1536
+ # "MAX" SP nz-number /
1537
+ # "ALL" SP sequence-set /
1538
+ # "COUNT" SP number /
1539
+ # search-ret-data-ext
1540
+ # ; All return data items conform to
1541
+ # ; search-ret-data-ext syntax.
1542
+ # search-ret-data-ext = search-modifier-name SP search-return-value
1543
+ # search-modifier-name = tagged-ext-label
1544
+ # search-return-value = tagged-ext-val
1545
+ #
1546
+ # From RFC4731 (ESEARCH):
1547
+ # search-return-data =/ "MODSEQ" SP mod-sequence-value
1548
+ #
1549
+ # From RFC9394 (PARTIAL):
1550
+ # search-return-data =/ ret-data-partial
1551
+ #
1552
+ def search_return_data
1553
+ label = search_modifier_name; SP!
1554
+ value =
1555
+ case label
1556
+ when "MIN" then nz_number
1557
+ when "MAX" then nz_number
1558
+ when "ALL" then sequence_set
1559
+ when "COUNT" then number
1560
+ when "MODSEQ" then mod_sequence_value # RFC7162: CONDSTORE
1561
+ when "PARTIAL" then ret_data_partial__value # RFC9394: PARTIAL
1562
+ else search_return_value
1563
+ end
1564
+ [label, value]
1565
+ end
1566
+
1567
+ # From RFC5267 (CONTEXT=SEARCH, CONTEXT=SORT) and RFC9394 (PARTIAL):
1568
+ # ret-data-partial = "PARTIAL"
1569
+ # SP "(" partial-range SP partial-results ")"
1570
+ def ret_data_partial__value
1571
+ lpar
1572
+ range = partial_range; SP!
1573
+ results = partial_results
1574
+ rpar
1575
+ ESearchResult::PartialResult.new(range, results)
1576
+ end
1577
+
1578
+ # partial-range = partial-range-first / partial-range-last
1579
+ # tagged-ext-simple =/ partial-range-last
1580
+ def partial_range
1581
+ case (str = atom)
1582
+ when Patterns::PARTIAL_RANGE_FIRST, Patterns::PARTIAL_RANGE_LAST
1583
+ min, max = [Integer($1), Integer($2)].minmax
1584
+ min..max
1585
+ else
1586
+ parse_error("unexpected atom %p, expected partial-range", str)
1587
+ end
1588
+ end
1589
+
1590
+ # partial-results = sequence-set / "NIL"
1591
+ # ;; <sequence-set> from [RFC3501].
1592
+ # ;; NIL indicates that no results correspond to
1593
+ # ;; the requested range.
1594
+ def partial_results; NIL? ? nil : sequence_set end
1595
+
1596
+ # search-modifier-name = tagged-ext-label
1597
+ alias search_modifier_name tagged_ext_label
1598
+
1599
+ # search-return-value = tagged-ext-val
1600
+ # ; Data for the returned search option.
1601
+ # ; A single "nz-number"/"number"/"number64" value
1602
+ # ; can be returned as an atom (i.e., without
1603
+ # ; quoting). A sequence-set can be returned
1604
+ # ; as an atom as well.
1605
+ def search_return_value; ExtensionData.new(tagged_ext_val) end
1606
+
1607
+ # search-correlator = SP "(" "TAG" SP tag-string ")"
1608
+ def search_correlator
1609
+ SP!; lpar; label("TAG"); SP!; tag = tag_string; rpar
1610
+ tag
1611
+ end
1612
+
1613
+ # tag-string = astring
1614
+ # ; <tag> represented as <astring>
1615
+ alias tag_string astring
1616
+
1478
1617
  # RFC5256: THREAD
1479
1618
  # thread-data = "THREAD" [SP 1*thread-list]
1480
1619
  def thread_data
@@ -1807,6 +1946,9 @@ module Net
1807
1946
  #
1808
1947
  # RFC8474: OBJECTID
1809
1948
  # resp-text-code =/ "MAILBOXID" SP "(" objectid ")"
1949
+ #
1950
+ # RFC9586: UIDONLY
1951
+ # resp-text-code =/ "UIDREQUIRED"
1810
1952
  def resp_text_code
1811
1953
  name = resp_text_code__name
1812
1954
  data =
@@ -1829,6 +1971,7 @@ module Net
1829
1971
  when "HIGHESTMODSEQ" then SP!; mod_sequence_value # CONDSTORE
1830
1972
  when "MODIFIED" then SP!; sequence_set # CONDSTORE
1831
1973
  when "MAILBOXID" then SP!; parens__objectid # RFC8474: OBJECTID
1974
+ when "UIDREQUIRED" then # RFC9586: UIDONLY
1832
1975
  else
1833
1976
  SP? and text_chars_except_rbra
1834
1977
  end
@@ -1858,11 +2001,10 @@ module Net
1858
2001
  #
1859
2002
  # n.b, uniqueid ⊂ uid-set. To avoid inconsistent return types, we always
1860
2003
  # match uid_set even if that returns a single-member array.
1861
- #
1862
2004
  def resp_code_apnd__data
1863
2005
  validity = number; SP!
1864
2006
  dst_uids = uid_set # uniqueid ⊂ uid-set
1865
- UIDPlusData.new(validity, nil, dst_uids)
2007
+ AppendUID(validity, dst_uids)
1866
2008
  end
1867
2009
 
1868
2010
  # already matched: "COPYUID"
@@ -1872,7 +2014,25 @@ module Net
1872
2014
  validity = number; SP!
1873
2015
  src_uids = uid_set; SP!
1874
2016
  dst_uids = uid_set
1875
- UIDPlusData.new(validity, src_uids, dst_uids)
2017
+ CopyUID(validity, src_uids, dst_uids)
2018
+ end
2019
+
2020
+ def AppendUID(...) DeprecatedUIDPlus(...) || AppendUIDData.new(...) end
2021
+ def CopyUID(...) DeprecatedUIDPlus(...) || CopyUIDData.new(...) end
2022
+
2023
+ # TODO: remove this code in the v0.6.0 release
2024
+ def DeprecatedUIDPlus(validity, src_uids = nil, dst_uids)
2025
+ return unless config.parser_use_deprecated_uidplus_data
2026
+ compact_uid_sets = [src_uids, dst_uids].compact
2027
+ count = compact_uid_sets.map { _1.count_with_duplicates }.max
2028
+ max = config.parser_max_deprecated_uidplus_data_size
2029
+ if count <= max
2030
+ src_uids &&= src_uids.each_ordered_number.to_a
2031
+ dst_uids = dst_uids.each_ordered_number.to_a
2032
+ UIDPlusData.new(validity, src_uids, dst_uids)
2033
+ elsif config.parser_use_deprecated_uidplus_data != :up_to_max_size
2034
+ parse_error("uid-set is too large: %d > %d", count, max)
2035
+ end
1876
2036
  end
1877
2037
 
1878
2038
  ADDRESS_REGEXP = /\G
@@ -1998,15 +2158,9 @@ module Net
1998
2158
  # uniqueid = nz-number
1999
2159
  # ; Strictly ascending
2000
2160
  def uid_set
2001
- token = match(T_NUMBER, T_ATOM)
2002
- case token.symbol
2003
- when T_NUMBER then [Integer(token.value)]
2004
- when T_ATOM
2005
- token.value.split(",").flat_map {|range|
2006
- range = range.split(":").map {|uniqueid| Integer(uniqueid) }
2007
- range.size == 1 ? range : Range.new(range.min, range.max).to_a
2008
- }
2009
- end
2161
+ set = sequence_set
2162
+ parse_error("uid-set cannot contain '*'") if set.include_star?
2163
+ set
2010
2164
  end
2011
2165
 
2012
2166
  def nil_atom
@@ -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://tools.ietf.org/html/rfc4505]. See
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://tools.ietf.org/html/rfc2822]).
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://tools.ietf.org/html/rfc4505]. To use
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
- # This API is *experimental*, and may change.
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
- # Create an AuthenticationExchange from a client adapter and a mechanism
14
- # authenticator:
15
- # def authenticate(mechanism, ...)
16
- # authenticator = SASL.authenticator(mechanism, ...)
17
- # SASL::AuthenticationExchange.new(
18
- # sasl_adapter, mechanism, authenticator
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
- # def sasl_adapter = MyClientAdapter.new(self, &method(:send_command))
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
- # SASL::AuthenticationExchange.build(sasl_adapter, ...)
29
- # .authenticate
28
+ # MyClient::SASLAdapter.new(self).authenticate(...)
30
29
  # end
31
30
  #
32
- # As a convenience, ::authenticate combines ::build and #authenticate:
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
- # Likewise, ClientAdapter#authenticate delegates to #authenticate:
38
- # def authenticate(...) = sasl_adapter.authenticate(...)
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
- # Use +registry+ to override the global Authenticators registry.
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 = -mechanism.to_s.upcase.tr(?_, ?-)
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 = -name.to_s.upcase.tr(?_, ?-)
86
+ key = Authenticators.normalize_name(name)
83
87
  @authenticators.delete(key)
84
88
  end
85
89
 
86
90
  def mechanism?(name)
87
- key = -name.to_s.upcase.tr(?_, ?-)
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 = -mechanism.to_s.upcase.tr(?_, ?-)
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