diff --git a/lib/net/imap.rb b/lib/net/imap.rb
index fb28f76a9..ebd452b24 100644
--- a/lib/net/imap.rb
+++ b/lib/net/imap.rb
@@ -80,11 +80,11 @@ module Net
# === Server capabilities and protocol extensions
#
# Net::IMAP does not modify its behavior 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
- # requirements for #starttls, #login, and #authenticate.
+ # #capabilities. Users of the class must check for required capabilities
+ # before issuing commands. Special care should be taken to follow all
+ # #capabilities requirements for #starttls, #login, and #authenticate.
#
- # See the #capability method for more information.
+ # See #capable?, #capabilities, and #capability for more information.
#
# == Examples of Usage
#
@@ -173,6 +173,8 @@ module Net
# == What's here?
#
# * {Connection control}[rdoc-ref:Net::IMAP@Connection+control+methods]
+ # * {Server capabilities}[rdoc-ref:Net::IMAP@Server+capabilities]
+ # * {Handling server responses}[rdoc-ref:Net::IMAP@Handling+server+responses]
# * {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]
@@ -180,7 +182,6 @@ module Net
# * {...for the "selected" state}[rdoc-ref:Net::IMAP@IMAP+commands+for+the+-22Selected-22+state]
# * {...for the "logout" state}[rdoc-ref:Net::IMAP@IMAP+commands+for+the+-22Logout-22+state]
# * {Supported IMAP extensions}[rdoc-ref:Net::IMAP@Supported+IMAP+extensions]
- # * {Handling server responses}[rdoc-ref:Net::IMAP@Handling+server+responses]
#
# === Connection control methods
#
@@ -191,6 +192,33 @@ module Net
# - #disconnect: Disconnects the connection (without sending #logout first).
# - #disconnected?: True if the connection has been closed.
#
+ # === Server capabilities
+ #
+ # - #capable?: Returns whether the server supports a given capability.
+ # - #capabilities: Returns the server's capabilities as a list of strings.
+ # - #auth_capable?: Returns whether the server supports a given SASL
+ # mechanism for use with the #authenticate command.
+ # - #clear_cached_capabilities: Clears cached capabilities.
+ #
+ # The capabilities cache is automatically cleared after completing
+ # #starttls, #login, or #authenticate.
+ # - #capability: Sends the +CAPABILITY+ command and returns the #capabilities.
+ #
+ # In general, #capable? should be used rather than explicitly sending a
+ # +CAPABILITY+ command to the server.
+ #
+ # === Handling server responses
+ #
+ # - #greeting: The server's initial untagged response, which can indicate a
+ # pre-authenticated connection.
+ # - #responses: Yields unhandled UntaggedResponse#data and non-+nil+
+ # ResponseCode#data.
+ # - #clear_responses: Deletes unhandled data from #responses and returns it.
+ # - #add_response_handler: Add a block to be called inside the receiver thread
+ # with every server response.
+ # - #response_handlers: Returns the list of response handlers.
+ # - #remove_response_handler: Remove a previously added response handler.
+ #
# === Core \IMAP commands
#
# The following commands are defined either by
@@ -227,8 +255,8 @@ module Net
#
# - #capability: Returns the server's capabilities as an array of strings.
#
- # Capabilities may change after #starttls, #authenticate, or #login
- # and cached capabilities must be reloaded.
+ # In general, #capable? should be used rather than explicitly sending a
+ # +CAPABILITY+ command to the server.
# - #noop: Allows the server to send unsolicited untagged #responses.
# - #logout: Tells the server to end the session. Enters the "_logout_" state.
#
@@ -506,18 +534,6 @@ module Net
# TODO...
#++
#
- # === Handling server responses
- #
- # - #greeting: The server's initial untagged response, which can indicate a
- # pre-authenticated connection.
- # - #responses: Yields unhandled UntaggedResponse#data and non-+nil+
- # ResponseCode#data.
- # - #clear_responses: Deletes unhandled data from #responses and returns it.
- # - #add_response_handler: Add a block to be called inside the receiver thread
- # with every server response.
- # - #response_handlers: Returns the list of response handlers.
- # - #remove_response_handler: Remove a previously added response handler.
- #
#
# == References
#--
@@ -803,63 +819,107 @@ def disconnected?
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.
+ # 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 used instead. They cache
+ # the capability result to avoid sending unnecessary commands. They also
+ # ensure cache invalidation is handled correctly.
+ #
+ # >>>
+ # *NOTE*: 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.
+ #
+ # Capability requirements—other than +IMAP4rev1+—are listed in the
+ # documentation for each command method.
+ #
+ # Related: #capable?, #capabilities, #enable
+ #
+ def capability
+ synchronize do
+ send_command("CAPABILITY")
+ @capabilities = @responses.delete("CAPABILITY").last.freeze
+ end
+ 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.
#
# >>>
- # *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.
+ # *NOTE*: 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.
#
- # Capability requirements—other than +IMAP4rev1+—are listed in the
- # documentation for each command method.
+ # Capability requirements—other than +IMAP4rev1+—are listed in the
+ # documentation for each command method.
#
- # Related: #enable
+ # Related: #capabilities, #capability, #enable
#
# ===== Basic IMAP4rev1 capabilities
#
- # All IMAP4rev1 servers must include +IMAP4rev1+ in their capabilities list.
- # All IMAP4rev1 servers must _implement_ the +STARTTLS+,
- # AUTH=PLAIN, and +LOGINDISABLED+ capabilities, and clients must
- # respect their presence or absence. See the capabilities requirements on
- # #starttls, #login, and #authenticate.
+ # IMAP4rev1 servers must include +IMAP4rev1+ in their capabilities list.
+ # IMAP4rev1 servers must _implement_ the +STARTTLS+, AUTH=PLAIN,
+ # and +LOGINDISABLED+ capabilities, and clients must respect their presence
+ # or absence. See the capabilities requirements on #starttls, #login, and
+ # #authenticate.
#
# ===== Using IMAP4rev1 extensions
#
- # 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.
+ # 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.
+ # 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.
#
# ===== Caching +CAPABILITY+ responses
#
- # 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.
+ # 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.
#
- # 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.
+ def capable?(capability) capabilities.include? capability.to_s.upcase end
+
+ # Returns the server capabilities. When available, cached capabilities are
+ # used without sending a new #capability command to the server.
#
- def capability
+ # To ensure case-insensitive capability comparison, use #capable? instead.
+ #
+ # Related: #capable?, #capability
+ def capabilities
+ @capabilities || capability
+ 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
- send_command("CAPABILITY")
- return @responses.delete("CAPABILITY")[-1]
+ clear_responses("CAPABILITY")
+ @capabilities = nil
end
end
@@ -870,8 +930,7 @@ def capability
# 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,
@@ -931,21 +990,17 @@ def logout
# >>>
# 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 must
- # *not* send capabilities in the #starttls response and clients must
- # not 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)
send_command("STARTTLS") do |resp|
@@ -956,11 +1011,30 @@ def starttls(options = {}, verify = true)
options = create_ssl_params(certs, verify)
rescue NoMethodError
end
+ clear_cached_capabilities
+ clear_responses
start_tls_session(options)
end
end
end
+ # Returns whether the server supports a given SASL +mechanism+ for use with
+ # the #authenticate command. The +mechanism+ is supported when
+ # #capabilities includes "AUTH=#{mechanism.to_s.upcase}". When
+ # available, cached capabilities are used without sending a new #capability
+ # command to the server.
+ #
+ # Per {[IMAP4rev1 §6.2.2]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.2.2],
+ #
+ # 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
+
# :call-seq:
# authenticate(mechanism, ...) -> ok_resp
# authenticate(mech, *creds, **props) {|prop, auth| val } -> ok_resp
@@ -1010,18 +1084,17 @@ def starttls(options = {}, verify = true)
# for information on these and other SASL mechanisms.
#
# ===== Capabilities
- #
- # Clients MUST NOT attempt to authenticate with a mechanism unless
- # "AUTH=#{mechanism}" for that mechanism is a server capability.
+ # Clients should not call #authenticate with mechanisms that are included in the server #capabilities as "AUTH=#{mechanism}".
#
# 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...
@@ -1030,18 +1103,17 @@ def starttls(options = {}, verify = true)
# password: proc { password ||= ui.prompt_for_password },
# oauth2_token: proc { accesstok ||= kms.fresh_access_token },
# }
- # capa = imap.capability
- # if capa.include? "AUTH=OAUTHBEARER"
+ # if auth_capable? "OAUTHBEARER"
# imap.authenticate "OAUTHBEARER", **creds # authcid, oauth2_token
- # elsif capa.include? "AUTH=XOAUTH2"
+ # elsif auth_capable? "XOAUTH2"
# imap.authenticate "XOAUTH2", **creds # authcid, oauth2_token
- # elsif capa.include? "AUTH=SCRAM-SHA-256"
+ # elsif auth_capable? "SCRAM-SHA-256"
# imap.authenticate "SCRAM-SHA-256", **creds # authcid, password
- # elsif capa.include? "AUTH=PLAIN"
+ # elsif auth_capable? "PLAIN"
# imap.authenticate "PLAIN", **creds # authcid, password
- # elsif capa.include? "AUTH=DIGEST-MD5"
+ # elsif auth_capable? "DIGEST-MD5"
# imap.authenticate "DIGEST-MD5", **creds # authcid, password
- # elsif capa.include? "LOGINDISABLED"
+ # elsif auth_capable? "LOGINDISABLED"
# raise "the server has disabled login"
# else
# imap.login username, password
@@ -1057,6 +1129,9 @@ def authenticate(mechanism, ...)
put_string(CRLF)
end
end
+ .tap { @capabilities = capabilites_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]
@@ -1072,8 +1147,8 @@ def authenticate(mechanism, ...)
# 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.
@@ -1082,6 +1157,7 @@ def authenticate(mechanism, ...)
#
def login(user, password)
send_command("LOGIN", user, password)
+ .tap { @capabilities = capabilites_from_resp_code _1 }
end
# Sends a {SELECT command [IMAP4rev1 §6.3.1]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.3.1]
@@ -1261,8 +1337,7 @@ def list(refname, mailbox)
#
# ===== 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."
@@ -1599,7 +1674,7 @@ def uid_expunge(uid_set)
# 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+, SEARCH=FUZZY, +OBJECTID+, or
# +SAVEDATE+. The following are some common search criteria:
#
@@ -1903,7 +1978,7 @@ def uid_thread(algorithm, search_keys, charset)
# The +ENABLE+ command is only valid in the _authenticated_ state, before
# any mailbox is selected.
#
- # Related: #capability
+ # Related: #capable?, #capabilities, #capability
#
# ===== Capabilities
#
@@ -1942,7 +2017,7 @@ def uid_thread(algorithm, search_keys, charset)
#
# ["UTF8=ONLY" [RFC6855[https://tools.ietf.org/html/rfc6855]]]
#
- # A server that reports the UTF8=ONLY #capability _requires_ that
+ # A server that reports the UTF8=ONLY capability _requires_ that
# the client enable("UTF8=ACCEPT") before any mailboxes may be
# selected. For convenience, enable("UTF8=ONLY") is aliased to
# enable("UTF8=ACCEPT").
@@ -2247,7 +2322,8 @@ def initialize(host, port_or_options = {},
if @greeting.nil?
raise Error, "connection closed"
end
- record_untagged_response_code(@greeting)
+ record_untagged_response_code @greeting
+ @capabilities = capabilites_from_resp_code @greeting
if @greeting.name == "BYE"
raise ByeResponseError, @greeting
end
@@ -2311,8 +2387,7 @@ def receive_responses
@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)
@@ -2390,20 +2465,31 @@ def get_response
return @parser.parse(buff)
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)
- if resp.data.instance_of?(ResponseText) &&
- (code = resp.data.code)
- record_response(code.name, code.data)
- end
+ 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)
+ def capabilites_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|
diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb
index 949710227..e11c55486 100644
--- a/test/net/imap/test_imap.rb
+++ b/test/net/imap/test_imap.rb
@@ -849,6 +849,249 @@ def test_uidplus_uidnotsticky
end
end
+ test "#capabilities returns cached CAPABILITY data" do
+ with_fake_server do |server, imap|
+ imap.clear_cached_capabilities
+ assert_empty server.commands
+ 10.times do
+ assert_equal(%w[IMAP4REV1 NAMESPACE MOVE IDLE UTF8=ACCEPT],
+ imap.capabilities)
+ end
+ assert_equal "CAPABILITY", server.commands.pop.name
+ assert_empty server.commands
+ end
+ end
+
+ test "#capable?(name) checks cached CAPABILITY data for name" do
+ with_fake_server do |server, imap|
+ imap.clear_cached_capabilities
+ assert_empty server.commands
+ 10.times do
+ assert imap.capable? "IMAP4rev1"
+ assert imap.capable? :NAMESPACE
+ assert imap.capable? "idle"
+ refute imap.capable? "LOGINDISABLED"
+ refute imap.capable? "auth=plain"
+ end
+ assert_equal "CAPABILITY", server.commands.pop.name
+ assert_empty server.commands
+ end
+ end
+
+ test "#auth_capable?(name) checks cached capabilities for AUTH=name" do
+ with_fake_server(
+ preauth: false, cleartext_auth: true,
+ sasl_mechanisms: %i[PLAIN SCRAM-SHA-1 SCRAM-SHA-256 XOAUTH2 OAUTHBEARER],
+ ) do |server, imap|
+ imap.clear_cached_capabilities
+ assert_empty server.commands
+ 10.times do
+ assert imap.auth_capable? :PLAIN
+ assert imap.auth_capable? "scram-sha-1"
+ assert imap.auth_capable? "OAuthBearer"
+ assert imap.auth_capable? :XOAuth2
+ refute imap.auth_capable? "EXTERNAL"
+ refute imap.auth_capable? :LOGIN
+ refute imap.auth_capable? "anonymous"
+ end
+ assert_equal "CAPABILITY", server.commands.pop.name
+ assert_empty server.commands
+ end
+ end
+
+ test "#clear_cached_capabilities clears cached capabilities" do
+ with_fake_server do |server, imap|
+ assert imap.capable?(:IMAP4rev1)
+ assert imap.capabilities_cached?
+ assert_empty server.commands
+ imap.clear_cached_capabilities
+ refute imap.capabilities_cached?
+ assert imap.capable?(:IMAP4rev1)
+ assert_equal "CAPABILITY", server.commands.pop.name
+ assert imap.capabilities_cached?
+ end
+ end
+
+ test "#capabilty caches its result" do
+ with_fake_server(greeting_capabilities: false) do |server, imap|
+ imap.capability
+ assert imap.capabilities_cached?
+ assert_equal "CAPABILITY", server.commands.pop.name
+ assert_empty server.commands
+ end
+ end
+
+ test "#capabilities caches greeting capabilities (cleartext)" do
+ with_fake_server(
+ preauth: false, cleartext_login: false, cleartext_auth: false,
+ ) do |server, imap|
+ assert imap.capabilities_cached?
+ assert_equal %w[IMAP4REV1 STARTTLS LOGINDISABLED], imap.capabilities
+ refute imap.auth_capable? "plain"
+ assert_empty server.commands
+ end
+ end
+
+ test "#capabilities caches greeting capabilities (PREAUTH)" do
+ with_fake_server(preauth: true) do |server, imap|
+ assert imap.capabilities_cached?
+ assert_equal %w[IMAP4REV1 NAMESPACE MOVE IDLE UTF8=ACCEPT],
+ imap.capabilities
+ assert_empty server.commands
+ end
+ end
+
+ if defined?(OpenSSL::SSL::SSLError)
+ test "#capabilities caches greeting capabilities (implicit TLS)" do
+ with_fake_server(preauth: false, implicit_tls: true) do |server, imap|
+ assert imap.capabilities_cached?
+ assert_equal %w[IMAP4REV1 AUTH=PLAIN], imap.capabilities
+ assert imap.auth_capable? "plain"
+ assert_empty server.commands
+ end
+ end
+
+ test "#capabilities cache is cleared after #starttls" do
+ with_fake_server(preauth: false, cleartext_auth: false) do |server, imap|
+ assert imap.capabilities_cached?
+ assert imap.capable? :IMAP4rev1
+ refute imap.auth_capable? "plain"
+
+ imap.starttls(ca_file: server.config.tls[:ca_file])
+ assert_equal "STARTTLS", server.commands.pop.name
+ refute imap.capabilities_cached?
+
+ assert imap.capable? :IMAP4rev1
+ assert imap.auth_capable? "plain"
+ assert_equal "CAPABILITY", server.commands.pop.name
+ assert imap.capabilities_cached?
+ assert_empty server.commands
+ end
+ end
+ end
+
+ test "#capabilities cache is cleared after #login" do
+ with_fake_server(preauth: false, cleartext_login: true) do |server, imap|
+ assert imap.capable? :IMAP4rev1
+ assert imap.capabilities_cached?
+
+ imap.login("test_user", "test-password")
+ assert_equal "LOGIN", server.commands.pop.name
+ refute imap.capabilities_cached?
+
+ assert imap.capable? :IMAP4rev1
+ assert_equal "CAPABILITY", server.commands.pop.name
+ assert imap.capabilities_cached?
+ assert_empty server.commands
+ end
+ end
+
+ test "#capabilities cache is cleared after #authenticate" do
+ with_fake_server(preauth: false, cleartext_auth: true) do |server, imap|
+ assert imap.capable?("AUTH=PLAIN")
+
+ imap.authenticate("PLAIN", "test_user", "test-password")
+ assert_equal "AUTHENTICATE", server.commands.pop.name
+ refute imap.capabilities_cached?
+
+ assert imap.capable? :IMAP4rev1
+ assert_equal "CAPABILITY", server.commands.pop.name
+ assert_empty server.commands
+ end
+ end
+
+ test "#capabilities cache IGNORES tagged OK response to STARTTLS" do
+ with_fake_server(preauth: false) do |server, imap|
+ server.on "STARTTLS" do |cmd|
+ cmd.done_ok code: "[CAPABILITY IMAP4rev1 AUTH=PLAIN fnord]"
+ server.state.use_tls
+ end
+
+ imap.starttls(ca_file: server.config.tls[:ca_file])
+ assert_equal "STARTTLS", server.commands.pop.name
+ refute imap.capabilities_cached?
+
+ refute imap.capable? "fnord"
+ assert_equal "CAPABILITY", server.commands.pop.name
+ end
+ end
+
+ test "#capabilities caches tagged OK response to LOGIN" do
+ with_fake_server(preauth: false, cleartext_login: true) do |server, imap|
+ server.on "LOGIN" do |cmd|
+ server.state.authenticate server.config.user
+ cmd.done_ok code: "[CAPABILITY IMAP4rev1 IMAP4rev2 MOVE NAMESPACE" \
+ " ENABLE IDLE UIDPLUS UNSELECT UTF8=ACCEPT]"
+ end
+
+ imap.login("test_user", "test-password")
+ assert_equal "LOGIN", server.commands.pop.name
+ assert imap.capabilities_cached?
+
+ assert imap.capable? :IMAP4rev1
+ assert imap.capable? :IMAP4rev2
+ assert imap.capable? "UIDPLUS"
+ assert_empty server.commands
+ end
+ end
+
+ test "#capabilities caches tagged OK response to AUTHENTICATE" do
+ with_fake_server(preauth: false, cleartext_login: true) do |server, imap|
+ server.on "AUTHENTICATE" do |cmd|
+ cmd.request_continuation ""
+ server.state.authenticate server.config.user
+ cmd.done_ok code: "[CAPABILITY IMAP4rev1 IMAP4rev2 MOVE NAMESPACE" \
+ " ENABLE IDLE UIDPLUS UNSELECT UTF8=ACCEPT]"
+ end
+
+ imap.authenticate("PLAIN", "test_user", "test-password")
+ assert_equal "AUTHENTICATE", server.commands.pop.name
+ assert imap.capabilities_cached?
+
+ assert imap.capable? :IMAP4rev1
+ assert imap.capable? :IMAP4rev2
+ assert imap.capable? "UIDPLUS"
+ assert_empty server.commands
+ end
+ end
+
+ test "#capabilities cache is NOT cleared after #login fails" do
+ with_fake_server(preauth: false, cleartext_auth: true) do |server, imap|
+ original_capabilities = imap.capabilities
+ begin
+ imap.login("wrong_user", "wrong-password")
+ rescue Net::IMAP::NoResponseError
+ end
+ assert_equal "LOGIN", server.commands.pop.name
+ assert_equal original_capabilities, imap.capabilities
+ assert_empty server.commands
+ end
+ end
+
+ test "#capabilities cache is NOT cleared after #authenticate fails" do
+ with_fake_server(preauth: false, cleartext_auth: true) do |server, imap|
+ original_capabilities = imap.capabilities
+ begin
+ imap.authenticate("PLAIN", "wrong_user", "wrong-password")
+ rescue Net::IMAP::NoResponseError
+ end
+ assert_equal "AUTHENTICATE", server.commands.pop.name
+ assert_equal original_capabilities, imap.capabilities
+ assert_empty server.commands
+ end
+ end
+
+ # NOTE: other recorded responses are cleared after #select
+ test "#capabilities cache is retained after selecting a mailbox" do
+ with_fake_server do |server, imap|
+ original_capabilities = imap.capabilities
+ imap.select "inbox"
+ assert_equal "SELECT", server.commands.pop.name
+ assert_equal original_capabilities, imap.capabilities
+ assert_empty server.commands
+ end
+ end
+
def test_enable
with_fake_server(
with_extensions: %i[ENABLE CONDSTORE UTF8=ACCEPT],