net-imap 0.4.17 → 0.5.2

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.

@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+ # An "extended search" response (+ESEARCH+). ESearchResult should be
6
+ # returned (instead of SearchResult) by IMAP#search, IMAP#uid_search,
7
+ # IMAP#sort, and IMAP#uid_sort under any of the following conditions:
8
+ #
9
+ # * Return options were specified for IMAP#search or IMAP#uid_search.
10
+ # The server must support a search extension which allows
11
+ # RFC4466[https://www.rfc-editor.org/rfc/rfc4466.html] +return+ options,
12
+ # such as +ESEARCH+, +PARTIAL+, or +IMAP4rev2+.
13
+ # * Return options were specified for IMAP#sort or IMAP#uid_sort.
14
+ # The server must support the +ESORT+ extension
15
+ # {[RFC5267]}[https://www.rfc-editor.org/rfc/rfc5267.html#section-3].
16
+ #
17
+ # *NOTE:* IMAP#search and IMAP#uid_search do not support +ESORT+ yet.
18
+ # * The server supports +IMAP4rev2+ but _not_ +IMAP4rev1+, or +IMAP4rev2+
19
+ # has been enabled. +IMAP4rev2+ requires +ESEARCH+ results.
20
+ #
21
+ # Note that some servers may claim to support a search extension which
22
+ # requires an +ESEARCH+ result, such as +PARTIAL+, but still only return a
23
+ # +SEARCH+ result when +return+ options are specified.
24
+ #
25
+ # Some search extensions may result in the server sending ESearchResult
26
+ # responses after the initiating command has completed. Use
27
+ # IMAP#add_response_handler to handle these responses.
28
+ class ESearchResult < Data.define(:tag, :uid, :data)
29
+ def initialize(tag: nil, uid: nil, data: nil)
30
+ tag => String | nil; tag = -tag if tag
31
+ uid => true | false | nil; uid = !!uid
32
+ data => Array | nil; data ||= []; data.freeze
33
+ super
34
+ end
35
+
36
+ # :call-seq: to_a -> Array of integers
37
+ #
38
+ # When #all contains a SequenceSet of message sequence
39
+ # numbers or UIDs, +to_a+ returns that set as an array of integers.
40
+ #
41
+ # When #all is +nil+, either because the server
42
+ # returned no results or because +ALL+ was not included in
43
+ # the IMAP#search +RETURN+ options, #to_a returns an empty array.
44
+ #
45
+ # Note that SearchResult also implements +to_a+, so it can be used without
46
+ # checking if the server returned +SEARCH+ or +ESEARCH+ data.
47
+ def to_a; all&.numbers || [] end
48
+
49
+ ##
50
+ # attr_reader: tag
51
+ #
52
+ # The tag string for the command that caused this response to be returned.
53
+ #
54
+ # When +nil+, this response was not caused by a particular command.
55
+
56
+ ##
57
+ # attr_reader: uid
58
+ #
59
+ # Indicates whether #data in this response refers to UIDs (when +true+) or
60
+ # to message sequence numbers (when +false+).
61
+
62
+ ##
63
+ alias uid? uid
64
+
65
+ ##
66
+ # attr_reader: data
67
+ #
68
+ # Search return data, as an array of <tt>[name, value]</tt> pairs. Most
69
+ # return data corresponds to a search +return+ option with the same name.
70
+ #
71
+ # Note that some return data names may be used more than once per result.
72
+ #
73
+ # This data can be more simply retrieved by #min, #max, #all, #count,
74
+ # #modseq, and other methods.
75
+
76
+ # :call-seq: min -> integer or nil
77
+ #
78
+ # The lowest message number/UID that satisfies the SEARCH criteria.
79
+ #
80
+ # Returns +nil+ when the associated search command has no results, or when
81
+ # the +MIN+ return option wasn't specified.
82
+ #
83
+ # Requires +ESEARCH+ {[RFC4731]}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.1] or
84
+ # +IMAP4rev2+ {[RFC9051]}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.3.4].
85
+ def min; data.assoc("MIN")&.last end
86
+
87
+ # :call-seq: max -> integer or nil
88
+ #
89
+ # The highest message number/UID that satisfies the SEARCH criteria.
90
+ #
91
+ # Returns +nil+ when the associated search command has no results, or when
92
+ # the +MAX+ return option wasn't specified.
93
+ #
94
+ # Requires +ESEARCH+ {[RFC4731]}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.1] or
95
+ # +IMAP4rev2+ {[RFC9051]}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.3.4].
96
+ def max; data.assoc("MAX")&.last end
97
+
98
+ # :call-seq: all -> sequence set or nil
99
+ #
100
+ # A SequenceSet containing all message sequence numbers or UIDs that
101
+ # satisfy the SEARCH criteria.
102
+ #
103
+ # Returns +nil+ when the associated search command has no results, or when
104
+ # the +ALL+ return option was not specified but other return options were.
105
+ #
106
+ # Requires +ESEARCH+ {[RFC4731]}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.1] or
107
+ # +IMAP4rev2+ {[RFC9051]}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.3.4].
108
+ #
109
+ # See also: #to_a
110
+ def all; data.assoc("ALL")&.last end
111
+
112
+ # :call-seq: count -> integer or nil
113
+ #
114
+ # Returns the number of messages that satisfy the SEARCH criteria.
115
+ #
116
+ # Returns +nil+ when the associated search command has no results, or when
117
+ # the +COUNT+ return option wasn't specified.
118
+ #
119
+ # Requires +ESEARCH+ {[RFC4731]}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.1] or
120
+ # +IMAP4rev2+ {[RFC9051]}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.3.4].
121
+ def count; data.assoc("COUNT")&.last end
122
+
123
+ # :call-seq: modseq -> integer or nil
124
+ #
125
+ # The highest +mod-sequence+ of all messages being returned.
126
+ #
127
+ # Returns +nil+ when the associated search command has no results, or when
128
+ # the +MODSEQ+ search criterion wasn't specified.
129
+ #
130
+ # Note that there is no search +return+ option for +MODSEQ+. It will be
131
+ # returned whenever the +CONDSTORE+ extension has been enabled. Using the
132
+ # +MODSEQ+ search criteria will implicitly enable +CONDSTORE+.
133
+ #
134
+ # Requires +CONDSTORE+ {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html]
135
+ # and +ESEARCH+ {[RFC4731]}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.2].
136
+ def modseq; data.assoc("MODSEQ")&.last end
137
+
138
+ end
139
+ end
140
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Net
4
4
  class IMAP < Protocol
5
+ autoload :ESearchResult, "#{__dir__}/esearch_result"
5
6
  autoload :FetchData, "#{__dir__}/fetch_data"
6
7
  autoload :SearchResult, "#{__dir__}/search_result"
7
8
  autoload :SequenceSet, "#{__dir__}/sequence_set"
@@ -939,7 +940,8 @@ module Net
939
940
  # for something else?
940
941
  #++
941
942
  def media_subtype
942
- warn("media_subtype is obsolete, use subtype instead.\n", uplevel: 1)
943
+ warn("media_subtype is obsolete, use subtype instead.\n",
944
+ uplevel: 1, category: :deprecated)
943
945
  return subtype
944
946
  end
945
947
  end
@@ -984,7 +986,8 @@ module Net
984
986
  # generate a warning message to +stderr+, then return
985
987
  # the value of +subtype+.
986
988
  def media_subtype
987
- warn("media_subtype is obsolete, use subtype instead.\n", uplevel: 1)
989
+ warn("media_subtype is obsolete, use subtype instead.\n",
990
+ uplevel: 1, category: :deprecated)
988
991
  return subtype
989
992
  end
990
993
  end
@@ -1040,77 +1043,6 @@ module Net
1040
1043
  end
1041
1044
  end
1042
1045
 
1043
- # BodyTypeAttachment is not used and will be removed in an upcoming release.
1044
- #
1045
- # === Bug Analysis
1046
- #
1047
- # \IMAP body structures are parenthesized lists and assign their fields
1048
- # positionally, so missing fields change the interpretation of all
1049
- # following fields. Additionally, different body types have a different
1050
- # number of required fields, followed by optional "extension" fields.
1051
- #
1052
- # BodyTypeAttachment was previously returned when a "message/rfc822" part,
1053
- # which should be sent as <tt>body-type-msg</tt> with ten required fields,
1054
- # was actually sent as a <tt>body-type-basic</tt> with _seven_ required
1055
- # fields.
1056
- #
1057
- # basic => type, subtype, param, id, desc, enc, octets, md5=nil, dsp=nil, lang=nil, loc=nil, *ext
1058
- # msg => type, subtype, param, id, desc, enc, octets, envelope, body, lines, md5=nil, ...
1059
- #
1060
- # Normally, +envelope+ and +md5+ are incompatible, but Net::IMAP leniently
1061
- # allowed buggy servers to send +NIL+ for +envelope+. As a result, when a
1062
- # server sent a <tt>message/rfc822</tt> part with +NIL+ for +md5+ and a
1063
- # non-<tt>NIL</tt> +dsp+, Net::IMAP misinterpreted the
1064
- # <tt>Content-Disposition</tt> as if it were a strange body type. In all
1065
- # reported cases, the <tt>Content-Disposition</tt> was "attachment", so
1066
- # BodyTypeAttachment was created as the workaround.
1067
- #
1068
- # === Current behavior
1069
- #
1070
- # When interpreted strictly, +envelope+ and +md5+ are incompatible. So the
1071
- # current parsing algorithm peeks ahead after it has received the seventh
1072
- # body field. If the next token is not the start of an +envelope+, we assume
1073
- # the server has incorrectly sent us a <tt>body-type-basic</tt> and return
1074
- # BodyTypeBasic. As a result, what was previously BodyTypeMessage#body =>
1075
- # BodyTypeAttachment is now BodyTypeBasic#disposition => ContentDisposition.
1076
- #
1077
- class BodyTypeAttachment < Struct.new(:dsp_type, :_unused_, :param)
1078
- # *invalid for BodyTypeAttachment*
1079
- def media_type
1080
- warn(<<~WARN, uplevel: 1)
1081
- BodyTypeAttachment#media_type is obsolete. Use dsp_type instead.
1082
- WARN
1083
- dsp_type
1084
- end
1085
-
1086
- # *invalid for BodyTypeAttachment*
1087
- def subtype
1088
- warn("BodyTypeAttachment#subtype is obsolete.\n", uplevel: 1)
1089
- nil
1090
- end
1091
-
1092
- ##
1093
- # method: dsp_type
1094
- # :call-seq: dsp_type -> string
1095
- #
1096
- # Returns the content disposition type, as defined by
1097
- # [DISPOSITION[https://tools.ietf.org/html/rfc2183]].
1098
-
1099
- ##
1100
- # method: param
1101
- # :call-seq: param -> hash
1102
- #
1103
- # Returns a hash representing parameters of the Content-Disposition
1104
- # field, as defined by [DISPOSITION[https://tools.ietf.org/html/rfc2183]].
1105
-
1106
- ##
1107
- def multipart?
1108
- return false
1109
- end
1110
- end
1111
-
1112
- deprecate_constant :BodyTypeAttachment
1113
-
1114
1046
  # Net::IMAP::BodyTypeMultipart represents body structures of messages and
1115
1047
  # message parts, when <tt>Content-Type</tt> is <tt>multipart/*</tt>.
1116
1048
  class BodyTypeMultipart < Struct.new(:media_type, :subtype,
@@ -1182,29 +1114,11 @@ module Net
1182
1114
  # generate a warning message to +stderr+, then return
1183
1115
  # the value of +subtype+.
1184
1116
  def media_subtype
1185
- warn("media_subtype is obsolete, use subtype instead.\n", uplevel: 1)
1117
+ warn("media_subtype is obsolete, use subtype instead.\n",
1118
+ uplevel: 1, category: :deprecated)
1186
1119
  return subtype
1187
1120
  end
1188
1121
  end
1189
1122
 
1190
- # === Obsolete
1191
- # BodyTypeExtension is not used and will be removed in an upcoming release.
1192
- #
1193
- # >>>
1194
- # BodyTypeExtension was (incorrectly) used for <tt>message/*</tt> parts
1195
- # (besides <tt>message/rfc822</tt>, which correctly uses BodyTypeMessage).
1196
- #
1197
- # Net::IMAP now (correctly) parses all message types (other than
1198
- # <tt>message/rfc822</tt> or <tt>message/global</tt>) as BodyTypeBasic.
1199
- class BodyTypeExtension < Struct.new(:media_type, :subtype,
1200
- :params, :content_id,
1201
- :description, :encoding, :size)
1202
- def multipart?
1203
- return false
1204
- end
1205
- end
1206
-
1207
- deprecate_constant :BodyTypeExtension
1208
-
1209
1123
  end
1210
1124
  end
@@ -185,6 +185,11 @@ module Net
185
185
  @str[@pos, str.length] == str
186
186
  end
187
187
 
188
+ def peek_re?(re)
189
+ assert_no_lookahead if Net::IMAP.debug
190
+ re.match?(@str, @pos)
191
+ end
192
+
188
193
  def peek_re(re)
189
194
  assert_no_lookahead if config.debug?
190
195
  re.match(@str, @pos)
@@ -769,7 +769,6 @@ module Net
769
769
  def response_data__ignored; response_data__unhandled(IgnoredResponse) end
770
770
  alias response_data__noop response_data__ignored
771
771
 
772
- alias esearch_response response_data__unhandled
773
772
  alias expunged_resp response_data__unhandled
774
773
  alias uidfetch_resp response_data__unhandled
775
774
  alias listrights_data response_data__unhandled
@@ -1317,31 +1316,19 @@ module Net
1317
1316
  # header-fld-name = astring
1318
1317
  #
1319
1318
  # NOTE: Previously, Net::IMAP recreated the raw original source string.
1320
- # Now, it grabs the raw encoded value using @str and @pos. A future
1321
- # version may simply return the decoded astring value. Although that is
1322
- # technically incompatible, it should almost never make a difference: all
1323
- # standard header field names are valid atoms:
1319
+ # Now, it returns the decoded astring value. Although this is technically
1320
+ # incompatible, it should almost never make a difference: all standard
1321
+ # header field names are valid atoms:
1324
1322
  #
1325
1323
  # https://www.iana.org/assignments/message-headers/message-headers.xhtml
1326
1324
  #
1327
- # Although RFC3501 allows any astring, RFC5322-valid header names are one
1328
- # or more of the printable US-ASCII characters, except SP and colon. So
1329
- # empty string isn't valid, and literals aren't needed and should not be
1330
- # used. This is explicitly unchanged by [I18N-HDRS] (RFC6532).
1331
- #
1332
- # RFC5233:
1325
+ # See also RFC5233:
1333
1326
  # optional-field = field-name ":" unstructured CRLF
1334
1327
  # field-name = 1*ftext
1335
1328
  # ftext = %d33-57 / ; Printable US-ASCII
1336
1329
  # %d59-126 ; characters not including
1337
1330
  # ; ":".
1338
- def header_fld_name
1339
- assert_no_lookahead
1340
- start = @pos
1341
- astring
1342
- end_pos = @token ? @pos - 1 : @pos
1343
- @str[start...end_pos]
1344
- end
1331
+ alias header_fld_name astring
1345
1332
 
1346
1333
  # mailbox-data = "FLAGS" SP flag-list / "LIST" SP mailbox-list /
1347
1334
  # "LSUB" SP mailbox-list / "SEARCH" *(SP nz-number) /
@@ -1480,6 +1467,78 @@ module Net
1480
1467
  end
1481
1468
  alias sort_data mailbox_data__search
1482
1469
 
1470
+ # esearch-response = "ESEARCH" [search-correlator] [SP "UID"]
1471
+ # *(SP search-return-data)
1472
+ # ;; Note that SEARCH and ESEARCH responses
1473
+ # ;; SHOULD be mutually exclusive,
1474
+ # ;; i.e., only one of the response types
1475
+ # ;; should be
1476
+ # ;; returned as a result of a command.
1477
+ # esearch-response = "ESEARCH" [search-correlator] [SP "UID"]
1478
+ # *(SP search-return-data)
1479
+ # ; ESEARCH response replaces SEARCH response
1480
+ # ; from IMAP4rev1.
1481
+ # search-correlator = SP "(" "TAG" SP tag-string ")"
1482
+ def esearch_response
1483
+ name = label("ESEARCH")
1484
+ tag = search_correlator if peek_str?(" (")
1485
+ uid = peek_re?(/\G UID\b/i) && (SP!; label("UID"); true)
1486
+ data = []
1487
+ data << search_return_data while SP?
1488
+ esearch = ESearchResult.new(tag, uid, data)
1489
+ UntaggedResponse.new(name, esearch, @str)
1490
+ end
1491
+
1492
+ # From RFC4731 (ESEARCH):
1493
+ # search-return-data = "MIN" SP nz-number /
1494
+ # "MAX" SP nz-number /
1495
+ # "ALL" SP sequence-set /
1496
+ # "COUNT" SP number /
1497
+ # search-ret-data-ext
1498
+ # ; All return data items conform to
1499
+ # ; search-ret-data-ext syntax.
1500
+ # search-ret-data-ext = search-modifier-name SP search-return-value
1501
+ # search-modifier-name = tagged-ext-label
1502
+ # search-return-value = tagged-ext-val
1503
+ #
1504
+ # From RFC4731 (ESEARCH):
1505
+ # search-return-data =/ "MODSEQ" SP mod-sequence-value
1506
+ #
1507
+ def search_return_data
1508
+ label = search_modifier_name; SP!
1509
+ value =
1510
+ case label
1511
+ when "MIN" then nz_number
1512
+ when "MAX" then nz_number
1513
+ when "ALL" then sequence_set
1514
+ when "COUNT" then number
1515
+ when "MODSEQ" then mod_sequence_value # RFC7162: CONDSTORE
1516
+ else search_return_value
1517
+ end
1518
+ [label, value]
1519
+ end
1520
+
1521
+ # search-modifier-name = tagged-ext-label
1522
+ alias search_modifier_name tagged_ext_label
1523
+
1524
+ # search-return-value = tagged-ext-val
1525
+ # ; Data for the returned search option.
1526
+ # ; A single "nz-number"/"number"/"number64" value
1527
+ # ; can be returned as an atom (i.e., without
1528
+ # ; quoting). A sequence-set can be returned
1529
+ # ; as an atom as well.
1530
+ def search_return_value; ExtensionData.new(tagged_ext_val) end
1531
+
1532
+ # search-correlator = SP "(" "TAG" SP tag-string ")"
1533
+ def search_correlator
1534
+ SP!; lpar; label("TAG"); SP!; tag = tag_string; rpar
1535
+ tag
1536
+ end
1537
+
1538
+ # tag-string = astring
1539
+ # ; <tag> represented as <astring>
1540
+ alias tag_string astring
1541
+
1483
1542
  # RFC5256: THREAD
1484
1543
  # thread-data = "THREAD" [SP 1*thread-list]
1485
1544
  def thread_data
@@ -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