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.

Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +7 -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 +402 -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 +126 -193
  18. data/lib/net/imap/response_parser/parser_utils.rb +11 -6
  19. data/lib/net/imap/response_parser.rb +159 -21
  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 +28 -24
  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/vanished_data.rb +56 -0
  40. data/lib/net/imap.rb +1001 -319
  41. data/net-imap.gemspec +3 -3
  42. data/rakelib/rfcs.rake +2 -0
  43. data/rakelib/string_prep_tables_generator.rb +2 -0
  44. metadata +11 -10
  45. data/.github/dependabot.yml +0 -6
  46. data/.github/workflows/pages.yml +0 -46
  47. data/.github/workflows/push_gem.yml +0 -48
  48. data/.github/workflows/test.yml +0 -31
  49. data/.gitignore +0 -12
  50. 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,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 # (draft) UIDONLY
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 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:
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
- # 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:
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
- def header_fld_name
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://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
@@ -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
- # An abstract base class for implementing a SASL authentication exchange.
12
- # Different clients will each have their own adapter subclass, overridden
13
- # to match their needs.
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
- # Although the default implementations _may_ be sufficient, subclasses
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
- attr_reader :client, :command_proc
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 should run a command with the arguments sent to it, yield each
26
- # continuation payload, respond to the server with the result of each
27
- # yield, and return the result. Non-successful results *MUST* raise an
28
- # exception. Exceptions in the block *MUST* cause the command to fail.
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
- # Subclasses that override #run_command may use #command_proc for
31
- # other purposes.
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
- # Delegates to AuthenticationExchange.authenticate.
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
- # Do the protocol and server both support an initial response?
40
- def sasl_ir_capable?; client.sasl_ir_capable? end
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
- # Does the server advertise support for the mechanism?
43
- def auth_capable?(mechanism); client.auth_capable?(mechanism) end
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
- # Runs the authenticate command with +mechanism+ and +initial_response+.
46
- # When +initial_response+ is nil, an initial response must NOT be sent.
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, &block)
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, &block)
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
- # Drop the connection gracefully.
65
- def drop_connection; client.drop_connection end
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://tools.ietf.org/html/rfc2195]. See Net::IMAP#authenticate.
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://tools.ietf.org/html/draft-ietf-sasl-crammd5-to-historic-00.html]
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://tools.ietf.org/html/rfc8314] discourage the use
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." # TODO: recommend SCRAM
23
+ warn "WARNING: CRAM-MD5 mechanism is deprecated.", category: :deprecated
24
24
  end
25
25
  require "digest/md5"
26
26
  @user = authcid || username || user