Skip to content

Commit 474fdbe

Browse files
committed
✨ Cache server capabilities; add #capable?(name)
Added methods: * `#capable?(name)` - the main API for discovering capabilities * `#capabilities` - calls `capability` when needed * `#capabilities_cached?` - whether capabilities are cached * `#clear_cached_capabilities` - clears the cache; is thread safe Fixes #31
1 parent b21e845 commit 474fdbe

File tree

2 files changed

+282
-49
lines changed

2 files changed

+282
-49
lines changed

lib/net/imap.rb

Lines changed: 115 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ module Net
173173
# == What's here?
174174
#
175175
# * {Connection control}[rdoc-ref:Net::IMAP@Connection+control+methods]
176+
# * {Server capabilities}[rdoc-ref:Net::IMAP@Server+capabilities]
176177
# * {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands]
177178
# * {...for any state}[rdoc-ref:Net::IMAP@IMAP+commands+for+any+state]
178179
# * {...for the "not authenticated" state}[rdoc-ref:Net::IMAP@IMAP+commands+for+the+-22Not+Authenticated-22+state]
@@ -191,6 +192,16 @@ module Net
191192
# - #disconnect: Disconnects the connection (without sending #logout first).
192193
# - #disconnected?: True if the connection has been closed.
193194
#
195+
# === Server capabilities
196+
#
197+
# - #capable?: Returns whether the server supports a given capability.
198+
# - #capabilities: Returns the server's capabilities as a list of strings.
199+
# - #clear_cached_capabilities: Clears cached capabilities.
200+
#
201+
# <em>The capabilities cache is automatically cleared after completing
202+
# #starttls, #login, or #authenticate.</em>
203+
# - #capability: Sends the +CAPABILITY+ command and returns the #capabilities.
204+
#
194205
# === Core \IMAP commands
195206
#
196207
# The following commands are defined either by
@@ -227,8 +238,8 @@ module Net
227238
#
228239
# - #capability: Returns the server's capabilities as an array of strings.
229240
#
230-
# <em>Capabilities may change after</em> #starttls, #authenticate, or #login
231-
# <em>and cached capabilities must be reloaded.</em>
241+
# <em>In general, #capable? should be used rather than explicitly sending a
242+
# +CAPABILITY+ command to the server.</em>
232243
# - #noop: Allows the server to send unsolicited untagged #responses.
233244
# - #logout: Tells the server to end the session. Enters the "_logout_" state.
234245
#
@@ -725,6 +736,9 @@ class IMAP < Protocol
725736
# Returns the initial greeting the server, an UntaggedResponse.
726737
attr_reader :greeting
727738

739+
# Implementation detail; only exposed for testing
740+
attr_reader :cached_capabilities # :nodoc:
741+
728742
# Seconds to wait until a connection is opened.
729743
# If the IMAP object cannot open a connection within this time,
730744
# it raises a Net::OpenTimeout exception. The default value is 30 seconds.
@@ -803,12 +817,11 @@ def disconnected?
803817
end
804818

805819
# Sends a {CAPABILITY command [IMAP4rev1 §6.1.1]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.1.1]
806-
# and returns an array of capabilities that the server supports. Each
807-
# capability is a string.
820+
# and returns an array of capabilities that are supported by the server.
821+
# Each capability is a string. Capabilities are case-insensitive.
808822
#
809-
# See the {IANA IMAP4 capabilities
810-
# registry}[http://www.iana.org/assignments/imap4-capabilities] for a list
811-
# of all standard capabilities, and their reference RFCs.
823+
# **NOTE**: Prefer to use #capable? or #capabilities instead, to avoid
824+
# sending unnecessary commands and correctly invalidate cached capabilities.
812825
#
813826
# >>>
814827
# <em>*Note* that Net::IMAP does not currently modify its
@@ -817,49 +830,94 @@ def disconnected?
817830
# a certain capability is supported by a server before
818831
# using it.</em>
819832
#
820-
# Capability requirements—other than +IMAP4rev1+—are listed in the
821-
# documentation for each command method.
833+
# Related: #capable?, #capabilities, #enable
822834
#
823-
# Related: #enable
835+
def capability
836+
synchronize do
837+
send_command("CAPABILITY")
838+
return @responses.delete("CAPABILITY")[-1]
839+
end
840+
end
841+
842+
# Returns whether the server supports a given capability. When available,
843+
# cached capabilities are used without sending a new #capability command to
844+
# the server.
845+
#
846+
# See the {IANA IMAP4 capabilities
847+
# registry}[http://www.iana.org/assignments/imap4-capabilities] for a list
848+
# of all standard capabilities, and their reference RFCs.
849+
#
850+
# >>>
851+
# *Note* that Net::IMAP does not <em>currently</em> modify its behaviour
852+
# according to the capabilities of the server; it is up to the user of the
853+
# class to ensure that a certain capability is supported by a server
854+
# before using it.
855+
#
856+
# Capability requirements—other than +IMAP4rev1+—are listed in the
857+
# documentation for each command method.
858+
#
859+
# Related: #capabilities, #capability, #enable
824860
#
825861
# ===== Basic IMAP4rev1 capabilities
826862
#
827-
# All IMAP4rev1 servers must include +IMAP4rev1+ in their capabilities list.
828-
# All IMAP4rev1 servers must _implement_ the +STARTTLS+,
829-
# <tt>AUTH=PLAIN</tt>, and +LOGINDISABLED+ capabilities, and clients must
830-
# respect their presence or absence. See the capabilities requirements on
831-
# #starttls, #login, and #authenticate.
863+
# IMAP4rev1 servers must include +IMAP4rev1+ in their capabilities list.
864+
# IMAP4rev1 servers must _implement_ the +STARTTLS+, <tt>AUTH=PLAIN</tt>,
865+
# and +LOGINDISABLED+ capabilities, and clients must respect their presence
866+
# or absence. See the capabilities requirements on #starttls, #login, and
867+
# #authenticate.
832868
#
833869
# ===== Using IMAP4rev1 extensions
834870
#
835-
# IMAP4rev1 servers must not activate incompatible behavior until an
836-
# explicit client action invokes a capability, e.g. sending a command or
837-
# command argument specific to that capability. Extensions with backward
838-
# compatible behavior, such as response codes or mailbox attributes, may
839-
# be sent at any time.
871+
# IMAP4rev1 servers must not activate behavior that is incompatible with the
872+
# base specification until an explicit client action invokes a capability,
873+
# e.g. sending a command or command argument specific to that capability.
874+
# Servers may send data with backward compatible behavior, such as response
875+
# codes or mailbox attributes, at any time without client action.
840876
#
841877
# Invoking capabilities which are unknown to Net::IMAP may cause unexpected
842878
# behavior and errors, for example ResponseParseError is raised when unknown
843879
# response syntax is received. Invoking commands or command parameters that
844880
# are unsupported by the server may raise NoResponseError, BadResponseError,
845881
# or cause other unexpected behavior.
846882
#
883+
# Some capabilities must be explicitly activated using the #enable command.
884+
# See #enable for more details.
885+
#
847886
# ===== Caching +CAPABILITY+ responses
848887
#
849-
# Servers may send their capability list, unsolicited, using the
850-
# +CAPABILITY+ response code or an untagged +CAPABILITY+ response. These
851-
# responses can be retrieved and cached using #responses or
852-
# #add_response_handler.
888+
# Servers may send their capability list unsolicited, using the +CAPABILITY+
889+
# response code or an untagged +CAPABILITY+ response. Cached capabilities
890+
# are discarded after #starttls, #login, or #authenticate. Both caching and
891+
# cache invalidation are handled internally by Net::IMAP.
853892
#
854-
# But cached capabilities _must_ be discarded after #starttls, #login, or
855-
# #authenticate. The OK TaggedResponse to #login and #authenticate may
856-
# include +CAPABILITY+ response code data, but the TaggedResponse for
857-
# #starttls is sent clear-text and cannot be trusted.
893+
def capable?(capability) capabilities.include? capability.to_s.upcase end
894+
895+
# Returns the server capabilities.
858896
#
859-
def capability
897+
# Cached capabilities are used without sending a new #capability command to
898+
# the server.
899+
#
900+
# In general, #capable? should be preferred because it doesn't rely on the
901+
# representation of capabilities as an array of uppercase strings.
902+
#
903+
# Related: #capable?, #capability
904+
def capabilities
905+
@cached_capabilities ||= capability.freeze
906+
end
907+
908+
# Returns whether capabilities have been cached. When true, #capable? and
909+
# #capabilities don't require sending a #capability command to the server.
910+
def capabilities_cached?
911+
!!@cached_capabilities
912+
end
913+
914+
# Clears capabilities that are currently cached by the Net::IMAP client.
915+
# This forces a #capability command to be sent the next time that #capable?
916+
# or #capabilities? are called.
917+
def clear_cached_capabilities
860918
synchronize do
861-
send_command("CAPABILITY")
862-
return @responses.delete("CAPABILITY")[-1]
919+
clear_responses("CAPABILITY")
920+
@cached_capabilities = nil
863921
end
864922
end
865923

@@ -870,8 +928,7 @@ def capability
870928
# Note that the user should first check if the server supports the ID
871929
# capability. For example:
872930
#
873-
# capabilities = imap.capability
874-
# if capabilities.include?("ID")
931+
# if capable?(:ID)
875932
# id = imap.id(
876933
# name: "my IMAP client (ruby)",
877934
# version: MyIMAP::VERSION,
@@ -940,15 +997,15 @@ def logout
940997
# The server's capabilities must include +STARTTLS+.
941998
#
942999
# Server capabilities may change after #starttls, #login, and #authenticate.
943-
# Cached capabilities _must_ be invalidated after this method completes.
1000+
# Cached capabilities are invalidated after this method completes.
9441001
#
9451002
# The TaggedResponse to #starttls is sent clear-text, so the server <em>must
9461003
# *not*</em> send capabilities in the #starttls response and clients <em>must
9471004
# not</em> use them if they are sent. Servers will generally send an
9481005
# unsolicited untagged response immeditely _after_ #starttls completes.
9491006
#
9501007
def starttls(options = {}, verify = true)
951-
send_command("STARTTLS") do |resp|
1008+
ok_response = send_command("STARTTLS") do |resp|
9521009
if resp.kind_of?(TaggedResponse) && resp.name == "OK"
9531010
begin
9541011
# for backward compatibility
@@ -959,6 +1016,8 @@ def starttls(options = {}, verify = true)
9591016
start_tls_session(options)
9601017
end
9611018
end
1019+
clear_cached_capabilities
1020+
ok_response
9621021
end
9631022

9641023
# :call-seq:
@@ -1015,9 +1074,9 @@ def starttls(options = {}, verify = true)
10151074
# <tt>"AUTH=#{mechanism}"</tt> for that mechanism is a server capability.
10161075
#
10171076
# Server capabilities may change after #starttls, #login, and #authenticate.
1018-
# Cached capabilities _must_ be invalidated after this method completes.
1019-
# The TaggedResponse to #authenticate may include updated capabilities in
1020-
# its ResponseCode.
1077+
# Cached capabilities are invalidated after this method completes. The
1078+
# TaggedResponse to #authenticate may include updated capabilities in its
1079+
# ResponseCode.
10211080
#
10221081
# ===== Example
10231082
# If the authenticators ignore unhandled keyword arguments, the same config
@@ -1030,33 +1089,35 @@ def starttls(options = {}, verify = true)
10301089
# password: proc { password ||= ui.prompt_for_password },
10311090
# oauth2_token: proc { accesstok ||= kms.fresh_access_token },
10321091
# }
1033-
# capa = imap.capability
1034-
# if capa.include? "AUTH=OAUTHBEARER"
1092+
# if capable? "AUTH=OAUTHBEARER"
10351093
# imap.authenticate "OAUTHBEARER", **creds # authcid, oauth2_token
1036-
# elsif capa.include? "AUTH=XOAUTH2"
1094+
# elsif capable? "AUTH=XOAUTH2"
10371095
# imap.authenticate "XOAUTH2", **creds # authcid, oauth2_token
1038-
# elsif capa.include? "AUTH=SCRAM-SHA-256"
1096+
# elsif capable? "AUTH=SCRAM-SHA-256"
10391097
# imap.authenticate "SCRAM-SHA-256", **creds # authcid, password
1040-
# elsif capa.include? "AUTH=PLAIN"
1098+
# elsif capable? "AUTH=PLAIN"
10411099
# imap.authenticate "PLAIN", **creds # authcid, password
1042-
# elsif capa.include? "AUTH=DIGEST-MD5"
1100+
# elsif capable? "AUTH=DIGEST-MD5"
10431101
# imap.authenticate "DIGEST-MD5", **creds # authcid, password
1044-
# elsif capa.include? "LOGINDISABLED"
1102+
# elsif capable? "LOGINDISABLED"
10451103
# raise "the server has disabled login"
10461104
# else
10471105
# imap.login username, password
10481106
# end
10491107
#
10501108
def authenticate(mechanism, ...)
10511109
authenticator = self.class.authenticator(mechanism, ...)
1052-
send_command("AUTHENTICATE", mechanism) do |resp|
1110+
ok_response = send_command("AUTHENTICATE", mechanism) do |resp|
10531111
if resp.instance_of?(ContinuationRequest)
10541112
data = authenticator.process(resp.data.text.unpack("m")[0])
10551113
s = [data].pack("m0")
10561114
send_string_data(s)
10571115
put_string(CRLF)
10581116
end
10591117
end
1118+
clear_cached_capabilities
1119+
# TODO: use capabilities from ok_response
1120+
ok_response
10601121
end
10611122

10621123
# Sends a {LOGIN command [IMAP4rev1 §6.2.3]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.2.3]
@@ -1081,7 +1142,10 @@ def authenticate(mechanism, ...)
10811142
# ResponseCode.
10821143
#
10831144
def login(user, password)
1084-
send_command("LOGIN", user, password)
1145+
ok_response = send_command("LOGIN", user, password)
1146+
clear_cached_capabilities
1147+
# TODO: use capabilities from ok_response
1148+
ok_response
10851149
end
10861150

10871151
# Sends a {SELECT command [IMAP4rev1 §6.3.1]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.3.1]
@@ -1261,8 +1325,7 @@ def list(refname, mailbox)
12611325
#
12621326
# ===== For example:
12631327
#
1264-
# capabilities = imap.capability
1265-
# if capabilities.include?("NAMESPACE")
1328+
# if capable?("NAMESPACE")
12661329
# namespaces = imap.namespace
12671330
# if namespace = namespaces.personal.first
12681331
# prefix = namespace.prefix # e.g. "" or "INBOX."
@@ -2394,6 +2457,9 @@ def record_untagged_response_code(resp)
23942457
if resp.data.instance_of?(ResponseText) &&
23952458
(code = resp.data.code)
23962459
record_response(code.name, code.data)
2460+
if code.name.casecmp?("CAPABILITY")
2461+
@cached_capabilities ||= code.data.freeze
2462+
end
23972463
end
23982464
end
23992465

0 commit comments

Comments
 (0)