net-imap 0.4.12 → 0.5.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +8 -1
  3. data/README.md +10 -4
  4. data/docs/styles.css +75 -14
  5. data/lib/net/imap/authenticators.rb +2 -2
  6. data/lib/net/imap/command_data.rb +61 -48
  7. data/lib/net/imap/config/attr_accessors.rb +75 -0
  8. data/lib/net/imap/config/attr_inheritance.rb +90 -0
  9. data/lib/net/imap/config/attr_type_coercion.rb +61 -0
  10. data/lib/net/imap/config.rb +470 -0
  11. data/lib/net/imap/data_encoding.rb +3 -3
  12. data/lib/net/imap/data_lite.rb +226 -0
  13. data/lib/net/imap/deprecated_client_options.rb +8 -5
  14. data/lib/net/imap/errors.rb +6 -0
  15. data/lib/net/imap/esearch_result.rb +180 -0
  16. data/lib/net/imap/fetch_data.rb +126 -47
  17. data/lib/net/imap/response_data.rb +124 -237
  18. data/lib/net/imap/response_parser/parser_utils.rb +11 -6
  19. data/lib/net/imap/response_parser.rb +187 -34
  20. data/lib/net/imap/sasl/anonymous_authenticator.rb +3 -3
  21. data/lib/net/imap/sasl/authentication_exchange.rb +52 -20
  22. data/lib/net/imap/sasl/authenticators.rb +8 -4
  23. data/lib/net/imap/sasl/client_adapter.rb +77 -26
  24. data/lib/net/imap/sasl/cram_md5_authenticator.rb +4 -4
  25. data/lib/net/imap/sasl/digest_md5_authenticator.rb +218 -56
  26. data/lib/net/imap/sasl/external_authenticator.rb +2 -2
  27. data/lib/net/imap/sasl/gs2_header.rb +7 -7
  28. data/lib/net/imap/sasl/login_authenticator.rb +4 -3
  29. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +6 -6
  30. data/lib/net/imap/sasl/plain_authenticator.rb +7 -7
  31. data/lib/net/imap/sasl/protocol_adapters.rb +60 -4
  32. data/lib/net/imap/sasl/scram_authenticator.rb +8 -8
  33. data/lib/net/imap/sasl.rb +7 -4
  34. data/lib/net/imap/sasl_adapter.rb +0 -1
  35. data/lib/net/imap/search_result.rb +2 -2
  36. data/lib/net/imap/sequence_set.rb +221 -82
  37. data/lib/net/imap/stringprep/nameprep.rb +1 -1
  38. data/lib/net/imap/stringprep/trace.rb +4 -4
  39. data/lib/net/imap/uidplus_data.rb +244 -0
  40. data/lib/net/imap/vanished_data.rb +56 -0
  41. data/lib/net/imap.rb +1010 -320
  42. data/net-imap.gemspec +3 -3
  43. data/rakelib/rfcs.rake +2 -0
  44. data/rakelib/string_prep_tables_generator.rb +2 -0
  45. metadata +12 -10
  46. data/.github/dependabot.yml +0 -6
  47. data/.github/workflows/pages.yml +0 -46
  48. data/.github/workflows/push_gem.yml +0 -48
  49. data/.github/workflows/test.yml +0 -31
  50. data/.gitignore +0 -12
  51. 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 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)) ")"
@@ -1314,30 +1358,19 @@ module Net
1314
1358
  # header-fld-name = astring
1315
1359
  #
1316
1360
  # NOTE: Previously, Net::IMAP recreated the raw original source string.
1317
- # Now, it grabs the raw encoded value using @str and @pos. A future
1318
- # version may simply return the decoded astring value. Although that is
1319
- # technically incompatible, it should almost never make a difference: all
1320
- # 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:
1321
1364
  #
1322
1365
  # https://www.iana.org/assignments/message-headers/message-headers.xhtml
1323
1366
  #
1324
- # Although RFC3501 allows any astring, RFC5322-valid header names are one
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:
1367
+ # See also RFC5233:
1330
1368
  # optional-field = field-name ":" unstructured CRLF
1331
1369
  # field-name = 1*ftext
1332
1370
  # ftext = %d33-57 / ; Printable US-ASCII
1333
1371
  # %d59-126 ; characters not including
1334
1372
  # ; ":".
1335
- def header_fld_name
1336
- assert_no_lookahead
1337
- start = @pos
1338
- astring
1339
- @str[start...@pos - 1]
1340
- end
1373
+ alias header_fld_name astring
1341
1374
 
1342
1375
  # mailbox-data = "FLAGS" SP flag-list / "LIST" SP mailbox-list /
1343
1376
  # "LSUB" SP mailbox-list / "SEARCH" *(SP nz-number) /
@@ -1476,6 +1509,111 @@ module Net
1476
1509
  end
1477
1510
  alias sort_data mailbox_data__search
1478
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
+
1479
1617
  # RFC5256: THREAD
1480
1618
  # thread-data = "THREAD" [SP 1*thread-list]
1481
1619
  def thread_data
@@ -1808,6 +1946,9 @@ module Net
1808
1946
  #
1809
1947
  # RFC8474: OBJECTID
1810
1948
  # resp-text-code =/ "MAILBOXID" SP "(" objectid ")"
1949
+ #
1950
+ # RFC9586: UIDONLY
1951
+ # resp-text-code =/ "UIDREQUIRED"
1811
1952
  def resp_text_code
1812
1953
  name = resp_text_code__name
1813
1954
  data =
@@ -1830,6 +1971,7 @@ module Net
1830
1971
  when "HIGHESTMODSEQ" then SP!; mod_sequence_value # CONDSTORE
1831
1972
  when "MODIFIED" then SP!; sequence_set # CONDSTORE
1832
1973
  when "MAILBOXID" then SP!; parens__objectid # RFC8474: OBJECTID
1974
+ when "UIDREQUIRED" then # RFC9586: UIDONLY
1833
1975
  else
1834
1976
  SP? and text_chars_except_rbra
1835
1977
  end
@@ -1859,11 +2001,10 @@ module Net
1859
2001
  #
1860
2002
  # n.b, uniqueid ⊂ uid-set. To avoid inconsistent return types, we always
1861
2003
  # match uid_set even if that returns a single-member array.
1862
- #
1863
2004
  def resp_code_apnd__data
1864
2005
  validity = number; SP!
1865
2006
  dst_uids = uid_set # uniqueid ⊂ uid-set
1866
- UIDPlusData.new(validity, nil, dst_uids)
2007
+ AppendUID(validity, dst_uids)
1867
2008
  end
1868
2009
 
1869
2010
  # already matched: "COPYUID"
@@ -1873,7 +2014,25 @@ module Net
1873
2014
  validity = number; SP!
1874
2015
  src_uids = uid_set; SP!
1875
2016
  dst_uids = uid_set
1876
- 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
1877
2036
  end
1878
2037
 
1879
2038
  ADDRESS_REGEXP = /\G
@@ -1999,15 +2158,9 @@ module Net
1999
2158
  # uniqueid = nz-number
2000
2159
  # ; Strictly ascending
2001
2160
  def uid_set
2002
- token = match(T_NUMBER, T_ATOM)
2003
- case token.symbol
2004
- when T_NUMBER then [Integer(token.value)]
2005
- when T_ATOM
2006
- token.value.split(",").flat_map {|range|
2007
- range = range.split(":").map {|uniqueid| Integer(uniqueid) }
2008
- range.size == 1 ? range : Range.new(range.min, range.max).to_a
2009
- }
2010
- end
2161
+ set = sequence_set
2162
+ parse_error("uid-set cannot contain '*'") if set.include_star?
2163
+ set
2011
2164
  end
2012
2165
 
2013
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