@@ -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