net-imap 0.4.12 → 0.5.5

Sign up to get free protection for your applications and to get access to all the features.

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