From b0f8b9ea40d9a941d494eceb92938d5895210897 Mon Sep 17 00:00:00 2001
From: nicholas evans <nicholas.evans@gmail.com>
Date: Sun, 16 Jul 2023 08:18:20 -0400
Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20cached=20#capabilities,=20#ca?=
 =?UTF-8?q?pable=3F(name),=20etc?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Updated methods:
* `#initialize` - save capabilities in `OK` or `PREAUTH` greeting
* `#capability` - always update saved capabilities
* `#starttls` - always clear capabilities after tagged OK response
* `#authenticate` - clear capabilities or update from tagged OK response
* `#login` - clear capabilities or update from tagged OK response

New methods:
* `#capable?(name)` - the primary API for discovering capabilities
* `#auth_capable?(name)` - returns whether a SASL mechanism is supported
* `#auth_mechanisms` - returns the server's supported SASL mechanisms
* `#capabilities` - cached version of `capability`
* `#capabilities_cached?` - whether capabilities are cached
* `#clear_cached_capabilities` - clears the cache

Also, the docs related to capabilities were reorganized and rewritten.

Fixes #31.

Reference: https://github.com/ruby/net-imap/commit/b0f8b9ea40d9a941d494eceb92938d5895210897
Conflict: (1) path remapped lib/net/imap.rb -> .bundle/gems/net-imap-0.3.8/lib/net/imap.rb (bundled gem v0.3.8 in ruby-3.2.8)
(2) test file hunk (test/net/imap/test_imap_capabilities.rb, new file) not modified (skipped per adaptation rule)
(3) "What's here?" / "Server capabilities" navigation rdoc hunks not adapted: v0.3.8 list structure differs from patch pre-image ("Handling server responses" is not a sibling list item in v0.3.8)
(4) #enable / UTF8=ONLY rdoc hunks not adapted: #enable method absent in v0.3.8 and UTF8 rdoc structure differs
(5) duplicated rdoc block above def capabilities_cached? in original patch (apparent copy-paste typo) was de-duplicated to a single copy
---
 .bundle/gems/net-imap-0.3.8/lib/net/imap.rb | 344 ++++++++++++++------
 1 file changed, 253 insertions(+), 91 deletions(-)

diff --git a/.bundle/gems/net-imap-0.3.8/lib/net/imap.rb b/.bundle/gems/net-imap-0.3.8/lib/net/imap.rb
index 0e5910cb4a69..9cc2fd67b58d 100644
--- a/.bundle/gems/net-imap-0.3.8/lib/net/imap.rb
+++ b/.bundle/gems/net-imap-0.3.8/lib/net/imap.rb
@@ -85,12 +85,83 @@ module Net
   #
   # === Server capabilities and protocol extensions
   #
-  # Net::IMAP <em>does not modify its behavior</em> according to server
-  # #capability.  Users of the class must check for required capabilities before
-  # issuing commands.  Special care should be taken to follow all #capability
+  # Net::IMAP does not _currently_ modify its behaviour according to the
+  # server's advertised #capabilities.  Users of this class must check that the
+  # server is capable of extension commands or command arguments before
+  # sending them.  Special care should be taken to follow the #capabilities
   # requirements for #starttls, #login, and #authenticate.
   #
-  # See the #capability method for more information.
+  # See #capable?, #auth_capable, #capabilities, #auth_mechanisms to discover
+  # server capabilities.  For relevant capability requirements, see the
+  # documentation on each \IMAP command.
+  #
+  #   imap = Net::IMAP.new("mail.example.com")
+  #   imap.capable?(:IMAP4rev1) or raise "Not an IMAP4rev1 server"
+  #   imap.capable?(:starttls)  or raise "Cannot start TLS"
+  #   imap.starttls
+  #
+  #   if imap.auth_capable?("PLAIN")
+  #     imap.authenticate "PLAIN", username, password
+  #   elsif !imap.capability?("LOGINDISABLED")
+  #     imap.login username, password
+  #   else
+  #     raise "No acceptable authentication mechanisms"
+  #   end
+  #
+  #   # Support for "UTF8=ACCEPT" implies support for "ENABLE"
+  #   imap.enable :utf8 if imap.auth_capable?("UTF8=ACCEPT")
+  #
+  #   namespaces  = imap.namespace if imap.capable?(:namespace)
+  #   mbox_prefix = namespaces&.personal&.first&.prefix || ""
+  #   mbox_delim  = namespaces&.personal&.first&.delim  || "/"
+  #   mbox_path   = prefix + %w[path to my mailbox].join(delim)
+  #   imap.create mbox_path
+  #
+  # === Basic IMAP4rev1 capabilities
+  #
+  # IMAP4rev1 servers must advertise +IMAP4rev1+ in their capabilities list.
+  # IMAP4rev1 servers must _implement_ the +STARTTLS+, <tt>AUTH=PLAIN</tt>,
+  # and +LOGINDISABLED+ capabilities.  See #starttls, #login, and #authenticate
+  # for the implications of these capabilities.
+  #
+  # === Caching +CAPABILITY+ responses
+  #
+  # Net::IMAP stores and discards capability
+  # data according to the requirements and recommendations in IMAP4rev2
+  # {§6.1.1}[https://www.rfc-editor.org/rfc/rfc9051#section-6.1.1],
+  # {§6.2}[https://www.rfc-editor.org/rfc/rfc9051#section-6.2], and
+  # {§7.1}[https://www.rfc-editor.org/rfc/rfc9051#section-7.1].
+  # Use #capable?, #auth_capable?, or #capabilities to this caching and avoid
+  # sending the #capability command unnecessarily.
+  #
+  # The server may advertise its initial capabilities using the +CAPABILITY+
+  # ResponseCode in a +PREAUTH+ or +OK+ #greeting.  When TLS has started
+  # (#starttls) and after authentication (#login or #authenticate), the server's
+  # capabilities may change and cached capabilities are discarded.  The server
+  # may send updated capabilities with an +OK+ TaggedResponse to #login or
+  # #authenticate, and these will be cached by Net::IMAP.  But the
+  # TaggedResponse to #starttls MUST be ignored--it is sent before TLS starts
+  # and is unprotected.
+  #
+  # When storing capability values to variables, be careful that they are
+  # discarded or reset appropriately, especially following #starttls.
+  #
+  # === Using IMAP4rev1 extensions
+  #
+  # IMAP4rev1 servers must not activate behavior that is incompatible with the
+  # base specification until an explicit client action invokes a capability,
+  # e.g. sending a command or command argument specific to that capability.
+  # Servers may send data with backward compatible behavior, such as response
+  # codes or mailbox attributes, at any time without client action.
+  #
+  # Invoking capabilities which are unknown to Net::IMAP may cause unexpected
+  # behavior and errors.  For example, ResponseParseError is raised when
+  # unknown response syntax is received.  Invoking commands or command
+  # parameters that are unsupported by the server may raise NoResponseError,
+  # BadResponseError, or cause other unexpected behavior.
+  #
+  # Some capabilities must be explicitly activated using the #enable command.
+  # See #enable for more details.
   #
   # == Examples of Usage
   #
@@ -214,6 +285,7 @@ module Net
   # == What's here?
   #
   # * {Connection control}[rdoc-ref:Net::IMAP@Connection+control+methods]
+  # * {Server capabilities}[rdoc-ref:Net::IMAP@Server+capabilities]
   # * {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands]
   #   * {...for any state}[rdoc-ref:Net::IMAP@IMAP+commands+for+any+state]
   #   * {...for the "not authenticated" state}[rdoc-ref:Net::IMAP@IMAP+commands+for+the+-22Not+Authenticated-22+state]
@@ -550,6 +622,23 @@ module Net
   # TODO...
   #++
   #
+  # === Server capabilities
+  #
+  # - #capable?: Returns whether the server supports a given capability.
+  # - #capabilities: Returns the server's capabilities as an array of strings.
+  # - #auth_capable?: Returns whether the server advertises support for a given
+  #   SASL mechanism, for use with #authenticate.
+  # - #auth_mechanisms: Returns the #authenticate SASL mechanisms which
+  #   the server claims to support as an array of strings.
+  # - #clear_cached_capabilities: Clears cached capabilities.
+  #
+  #   <em>The capabilities cache is automatically cleared after completing
+  #   #starttls, #login, or #authenticate.</em>
+  # - #capability: Sends the +CAPABILITY+ command and returns the #capabilities.
+  #
+  #   <em>In general, #capable? should be used rather than explicitly sending a
+  #   +CAPABILITY+ command to the server.</em>
+  #
   # === Handling server responses
   #
   # - #greeting: The server's initial untagged response, which can indicate a
@@ -873,62 +962,128 @@ module Net
       return @sock.closed?
     end
 
-    # Sends a {CAPABILITY command [IMAP4rev1 §6.1.1]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.1.1]
-    # and returns an array of capabilities that the server supports.  Each
-    # capability is a string.
+    # Returns the server capabilities.  When available, cached capabilities are
+    # used without sending a new #capability command to the server.
+    #
+    # To ensure case-insensitive capability comparison, use #capable? instead.
+    #
+    # Related: #capable?, #auth_capable?, #capability
+    def capabilities
+      @capabilities || capability
+    end
+
+    # Returns whether the server supports a given +capability+.  When available,
+    # cached #capabilities are used without sending a new #capability command to
+    # the server.
     #
     # See the {IANA IMAP4 capabilities
     # registry}[http://www.iana.org/assignments/imap4-capabilities] for a list
     # of all standard capabilities, and their reference RFCs.
     #
     # >>>
-    #   <em>*Note* that Net::IMAP does not currently modify its
-    #   behaviour according to the capabilities of the server;
-    #   it is up to the user of the class to ensure that
-    #   a certain capability is supported by a server before
-    #   using it.</em>
+    #   <em>*NOTE:* Net::IMAP does not _currently_ modify its behaviour
+    #   according to the server's advertised capabilities.  Users of this class
+    #   must check that the server is #capable? of extension commands or command
+    #   arguments before sending them.</em>
+    #
+    #   <em>Capability requirements—other than +IMAP4rev1+—are listed in the
+    #   documentation for each command method.</em>
+    #
+    # Related: #auth_capable?, #capabilities, #capability, #enable
     #
-    # Capability requirements—other than +IMAP4rev1+—are listed in the
-    # documentation for each command method.
+    # ===== Caching +CAPABILITY+ responses
     #
-    # ===== Basic IMAP4rev1 capabilities
+    # Servers may send their capability list unsolicited, using the +CAPABILITY+
+    # response code or an untagged +CAPABILITY+ response.  Cached capabilities
+    # are discarded after #starttls, #login, or #authenticate.  Caching and
+    # cache invalidation are handled internally by Net::IMAP.
     #
-    # All IMAP4rev1 servers must include +IMAP4rev1+ in their capabilities list.
-    # All IMAP4rev1 servers must _implement_ the +STARTTLS+,
-    # <tt>AUTH=PLAIN</tt>, and +LOGINDISABLED+ capabilities, and clients must
-    # respect their presence or absence.  See the capabilites requirements on
-    # #starttls, #login, and #authenticate.
+    def capable?(capability) capabilities.include? capability.to_s.upcase end
+    alias capability? capable?
+
+    # Returns the #authenticate mechanisms that the server claims to support.
+    # These are derived from the #capabilities with an <tt>AUTH=</tt> prefix.
     #
-    # ===== Using IMAP4rev1 extensions
+    # This may be different when the connection is cleartext or using TLS.  Most
+    # servers will drop all <tt>AUTH=</tt> mechanisms from #capabilities after
+    # the connection has authenticated.
     #
-    # IMAP4rev1 servers must not activate incompatible behavior until an
-    # explicit client action invokes a capability, e.g. sending a command or
-    # command argument specific to that capability.  Extensions with backward
-    # compatible behavior, such as response codes or mailbox attributes, may
-    # be sent at any time.
+    # Related: #auth_capable?, #capabilities
     #
-    # Invoking capabilities which are unknown to Net::IMAP may cause unexpected
-    # behavior and errors, for example ResponseParseError is raised when unknown
-    # response syntax is received.  Invoking commands or command parameters that
-    # are unsupported by the server may raise NoResponseError, BadResponseError,
-    # or cause other unexpected behavior.
+    # ===== Example
     #
-    # ===== Caching +CAPABILITY+ responses
+    #    imap = Net::IMAP.new(hostname, ssl: false)
+    #    imap.capabilities    # => ["IMAP4REV1", "LOGINDISABLED"]
+    #    imap.auth_mechanisms # => []
+    #    imap.starttls
+    #    imap.capabilities    # => ["IMAP4REV1", "AUTH=PLAIN", "AUTH=XOAUTH2",
+    #                               "AUTH=OAUTHBEARER", "AUTH=SCRAM-SHA-256"]
+    #    imap.auth_mechanisms # => ["PLAIN", "XOAUTH2", "SCRAM-SHA-256"]
+    #    imap.authenticate("OAUTHBEARER", username, oauth2_access_token)
+    #    imap.auth_mechanisms # => []
+    #
+    def auth_mechanisms
+      capabilities
+        .grep(/\AAUTH=/i)
+        .map { _1.delete_prefix("AUTH=") }
+    end
+
+    # Returns whether the server supports a given SASL +mechanism+ for use with
+    # the #authenticate command.  The +mechanism+ is supported when
+    # #capabilities includes <tt>"AUTH=#{mechanism.to_s.upcase}"</tt>.  When
+    # available, cached capabilities are used without sending a new #capability
+    # command to the server.
     #
-    # Servers may send their capability list, unsolicited, using the
-    # +CAPABILITY+ response code or an untagged +CAPABILITY+ response.  These
-    # responses can be retrieved and cached using #responses or
-    # #add_response_handler.
+    # Per {[IMAP4rev1 §6.2.2]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.2.2],
     #
-    # But cached capabilities _must_ be discarded after #starttls, #login, or
-    # #authenticate.  The OK TaggedResponse to #login and #authenticate may
-    # include +CAPABILITY+ response code data, but the TaggedResponse for
-    # #starttls is sent clear-text and cannot be trusted.
+    #   imap.capable?      "AUTH=PLAIN"  # => true
+    #   imap.auth_capable? "PLAIN"       # => true
+    #   imap.auth_capable? "blurdybloop" # => false
+    #
+    # Related: #authenticate, #capable?, #capabilities
+    def auth_capable?(mechanism)
+      capable? "AUTH=#{mechanism}"
+    end
+
+    # Returns whether capabilities have been cached.  When true, #capable? and
+    # #capabilities don't require sending a #capability command to the server.
+    def capabilities_cached?
+      !!@capabilities
+    end
+
+    # Clears capabilities that are currently cached by the Net::IMAP client.
+    # This forces a #capability command to be sent the next time that #capable?
+    # or #capabilities? are called.
+    def clear_cached_capabilities
+      synchronize do
+        clear_responses("CAPABILITY")
+        @capabilities = nil
+      end
+    end
+
+    # Sends a {CAPABILITY command [IMAP4rev1 §6.1.1]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.1.1]
+    # and returns an array of capabilities that are supported by the server.
+    # The result will be stored for use by #capable? and #capabilities.
+    #
+    # In general, #capable? or #capabilities should be used instead.  They cache
+    # the capability result to avoid sending unnecessary commands.  They also
+    # ensure cache invalidation is handled correctly.
+    #
+    # >>>
+    #   <em>*NOTE:* Net::IMAP does not _currently_ modify its behaviour
+    #   according to the server's advertised capabilities.  Users of this class
+    #   must check that the server is #capable? of extension commands or command
+    #   arguments before sending them.</em>
+    #
+    #   <em>Capability requirements—other than +IMAP4rev1+—are listed in the
+    #   documentation for each command method.</em>
+    #
+    # Related: #capable?, #auth_capable?, #capability, #enable
     #
     def capability
       synchronize do
         send_command("CAPABILITY")
-        return @responses.delete("CAPABILITY")[-1]
+        @capabilities = @responses.delete("CAPABILITY").last.freeze
       end
     end
 
@@ -939,8 +1094,7 @@ module Net
     # Note that the user should first check if the server supports the ID
     # capability. For example:
     #
-    #    capabilities = imap.capability
-    #    if capabilities.include?("ID")
+    #    if capable?(:ID)
     #      id = imap.id(
     #        name: "my IMAP client (ruby)",
     #        version: MyIMAP::VERSION,
@@ -1000,21 +1154,17 @@ module Net
     # >>>
     #   Any #response_handlers added before STARTTLS should be aware that the
     #   TaggedResponse to STARTTLS is sent clear-text, _before_ TLS negotiation.
-    #   TLS negotiation starts immediately after that response.
+    #   TLS starts immediately _after_ that response.  Any response code sent
+    #   with the response (e.g. CAPABILITY) is insecure and cannot be trusted.
     #
     # Related: Net::IMAP.new, #login, #authenticate
     #
     # ===== Capability
-    #
-    # The server's capabilities must include +STARTTLS+.
+    # Clients should not call #starttls unless the server advertises the
+    # +STARTTLS+ capability.
     #
     # Server capabilities may change after #starttls, #login, and #authenticate.
-    # Cached capabilities _must_ be invalidated after this method completes.
-    #
-    # The TaggedResponse to #starttls is sent clear-text, so the server <em>must
-    # *not*</em> send capabilities in the #starttls response and clients <em>must
-    # not</em> use them if they are sent.  Servers will generally send an
-    # unsolicited untagged response immeditely _after_ #starttls completes.
+    # Cached #capabilities will be cleared when this method completes.
     #
     def starttls(options = {}, verify = true)
       handled = false
@@ -1028,6 +1178,8 @@ module Net
             options = create_ssl_params(certs, verify)
           rescue NoMethodError
           end
+          clear_cached_capabilities
+          clear_responses
           start_tls_session(options)
         end
       rescue Exception => error
@@ -1096,18 +1248,17 @@ module Net
     # for information on these and other SASL mechanisms.
     #
     # ===== Capabilities
-    #
-    # Clients MUST NOT attempt to authenticate with a mechanism unless
-    # <tt>"AUTH=#{mechanism}"</tt> for that mechanism is a server capability.
+    # Clients should not call #authenticate with mechanisms that are included in the server #capabilities as <tt>"AUTH=#{mechanism}"</tt>.
     #
     # Server capabilities may change after #starttls, #login, and #authenticate.
-    # Cached capabilities _must_ be invalidated after this method completes.
-    # The TaggedResponse to #authenticate may include updated capabilities in
-    # its ResponseCode.
+    # Cached #capabilities will be cleared when this method completes.
+    # If the TaggedResponse to #authenticate includes updated capabilities, they
+    # will be cached.
     #
     # ===== Example
-    # If the authenticators ignore unhandled keyword arguments, the same config
-    # can be used for multiple mechanisms:
+    # Use auth_capable? to discover which mechanisms are suuported by the
+    # server.  For authenticators that ignore unhandled keyword arguments, the
+    # same config can be used for multiple mechanisms:
     #
     #    password  = nil # saved locally, so we don't ask more than once
     #    accesstok = nil # saved locally...
@@ -1116,18 +1267,16 @@ module Net
     #      password:     proc { password  ||= ui.prompt_for_password },
     #      oauth2_token: proc { accesstok ||= kms.fresh_access_token },
     #    }
-    #    capa = imap.capability
-    #    if    capa.include? "AUTH=OAUTHBEARER"
-    #      imap.authenticate "OAUTHBEARER",   **creds # authcid, oauth2_token
-    #    elsif capa.include? "AUTH=XOAUTH2"
-    #      imap.authenticate "XOAUTH2",       **creds # authcid, oauth2_token
-    #    elsif capa.include? "AUTH=SCRAM-SHA-256"
-    #      imap.authenticate "SCRAM-SHA-256", **creds # authcid, password
-    #    elsif capa.include? "AUTH=PLAIN"
-    #      imap.authenticate "PLAIN",         **creds # authcid, password
-    #    elsif capa.include? "AUTH=DIGEST-MD5"
-    #      imap.authenticate "DIGEST-MD5",    **creds # authcid, password
-    #    elsif capa.include? "LOGINDISABLED"
+    #    mechanism = %w[
+    #      OAUTHBEARER XOAUTH2
+    #      SCRAM-SHA-256 SCRAM-SHA-1
+    #      PLAIN
+    #    ].find {|m|
+    #      imap.auth_capable?(m)
+    #    }
+    #    if mechanism
+    #      imap.authenticate mechanism, **creds
+    #    elsif capable? "LOGINDISABLED"
     #      raise "the server has disabled login"
     #    else
     #      imap.login username, password
@@ -1143,6 +1292,9 @@ module Net
           put_string(CRLF)
         end
       end
+        .tap { @capabilities = capabilities_from_resp_code _1 }
+      # NOTE: If any Net::IMAP::SASL mechanism ever supports security layer
+      # negotiation, capabilities sent during the "OK" response MUST be ignored.
     end
 
     # Sends a {LOGIN command [IMAP4rev1 §6.2.3]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.2.3]
@@ -1158,8 +1310,8 @@ module Net
     # Related: #authenticate, #starttls
     #
     # ==== Capabilities
-    # Clients MUST NOT call #login if +LOGINDISABLED+ is listed with the
-    # capabilities.
+    # An IMAP client MUST NOT call #login unless the server advertises the
+    # +LOGINDISABLED+ capability.
     #
     # Server capabilities may change after #starttls, #login, and #authenticate.
     # Cached capabilities _must_ be invalidated after this method completes.
@@ -1168,6 +1320,7 @@ module Net
     #
     def login(user, password)
       send_command("LOGIN", user, password)
+        .tap { @capabilities = capabilities_from_resp_code _1 }
     end
 
     # Sends a {SELECT command [IMAP4rev1 §6.3.1]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.3.1]
@@ -1347,8 +1500,7 @@ module Net
     #
     # ===== For example:
     #
-    #    capabilities = imap.capability
-    #    if capabilities.include?("NAMESPACE")
+    #    if capable?("NAMESPACE")
     #      namespaces = imap.namespace
     #      if namespace = namespaces.personal.first
     #        prefix = namespace.prefix  # e.g. "" or "INBOX."
@@ -1695,7 +1847,7 @@ module Net
     # or  [{IMAP4rev2 §6.4.4}[https://www.rfc-editor.org/rfc/rfc9051.html#section-6.4.4]],
     # in addition to documentation for
     # any [CAPABILITIES[https://www.iana.org/assignments/imap-capabilities/imap-capabilities.xhtml]]
-    # reported by #capability which may define additional search filters, e.g:
+    # reported by #capabilities which may define additional search filters, e.g:
     # +CONDSTORE+, +WITHIN+, +FILTERS+, <tt>SEARCH=FUZZY</tt>, +OBJECTID+, or
     # +SAVEDATE+.  The following are some common search criteria:
     #
@@ -2275,7 +2427,8 @@ module Net
         if @greeting.nil?
           raise Error, "connection closed"
         end
-        record_untagged_response_code(@greeting)
+        record_untagged_response_code @greeting
+        @capabilities = capabilities_from_resp_code @greeting
         if @greeting.name == "BYE"
           raise ByeResponseError, @greeting
         end
@@ -2340,8 +2493,7 @@ module Net
                 @continuation_request_arrival.signal
               end
             when UntaggedResponse
-              record_response(resp.name, resp.data)
-              record_untagged_response_code(resp)
+              record_untagged_response(resp)
               if resp.name == "BYE" && @logout_command_tag.nil?
                 @sock.close
                 @exception = ByeResponseError.new(resp)
@@ -2406,22 +2558,32 @@ module Net
       @parser.parse(buff)
     end
 
-    def record_untagged_response_code(resp)
-      if resp.data.instance_of?(ResponseText) &&
-          (code = resp.data.code)
-        record_response(code.name, code.data)
-      end
+    #############################
+    # built-in response handlers
+
+    # store name => [..., data]
+    def record_untagged_response(resp)
+      @responses[resp.name] << resp.data
+      record_untagged_response_code resp
     end
 
-    #############################
+    # store code.name => [..., code.data]
+    def record_untagged_response_code(resp)
+      return unless resp.data.is_a?(ResponseText)
+      return unless (code = resp.data.code)
+      @responses[code.name] << code.data
+    end
 
-    def record_response(name, data)
-      unless @responses.has_key?(name)
-        @responses[name] = []
-      end
-      @responses[name].push(data)
+    # NOTE: only call this for greeting, login, and authenticate
+    def capabilities_from_resp_code(resp)
+      return unless %w[PREAUTH OK].any? { _1.casecmp? resp.name }
+      return unless (code = resp.data.code)
+      return unless code.name.casecmp?("CAPABILITY")
+      code.data.freeze
     end
 
+    #############################
+
     def send_command(cmd, *args, &block)
       synchronize do
         args.each do |i|
-- 
2.43.0