From 24c5f0f35db06eda2df6a6909577ea54ea9c7b83 Mon Sep 17 00:00:00 2001 From: nicholas evans Date: Wed, 19 Jul 2023 23:10:05 -0400 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=94=87=20Silence=20ivar=20warnings=20?= =?UTF-8?q?in=202.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/net/imap/fake_server/socket.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/net/imap/fake_server/socket.rb b/test/net/imap/fake_server/socket.rb index a187b0e4..21795aa5 100644 --- a/test/net/imap/fake_server/socket.rb +++ b/test/net/imap/fake_server/socket.rb @@ -10,6 +10,8 @@ class Socket def initialize(tcp_socket, config:) @config = config @tcp_socket = tcp_socket + @tls_socket = nil + @closed = false use_tls if config.implicit_tls && tcp_socket end From 25d75d45875366eaffaeda1400436c87213d3c16 Mon Sep 17 00:00:00 2001 From: nick evans Date: Sat, 22 Jul 2023 10:13:26 -0400 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=A7=AA=20Add=20Net::IMAP::FakeServer:?= =?UTF-8?q?:TestHelper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This simplifies using Net::IMAP::FakeServer in multiple test suites. --- test/net/imap/fake_server.rb | 1 + test/net/imap/fake_server/test_helper.rb | 29 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 test/net/imap/fake_server/test_helper.rb diff --git a/test/net/imap/fake_server.rb b/test/net/imap/fake_server.rb index db9b5e37..ea561ff7 100644 --- a/test/net/imap/fake_server.rb +++ b/test/net/imap/fake_server.rb @@ -42,6 +42,7 @@ class Net::IMAP::FakeServer autoload :ResponseWriter, "#{dir}/response_writer" autoload :Socket, "#{dir}/socket" autoload :Session, "#{dir}/session" + autoload :TestHelper, "#{dir}/test_helper" # Returns the server's FakeServer::Configuration attr_reader :config diff --git a/test/net/imap/fake_server/test_helper.rb b/test/net/imap/fake_server/test_helper.rb new file mode 100644 index 00000000..d9777c6f --- /dev/null +++ b/test/net/imap/fake_server/test_helper.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative "../fake_server" + +module Net::IMAP::FakeServer::TestHelper + + def with_fake_server(select: nil, timeout: 5, **opts) + Timeout.timeout(timeout) do + server = Net::IMAP::FakeServer.new(timeout: timeout, **opts) + @threads << Thread.new do server.run end if @threads + tls = opts[:implicit_tls] + tls = {ca_file: server.config.tls[:ca_file]} if tls == true + client = Net::IMAP.new("localhost", port: server.port, ssl: tls) + begin + if select + client.select(select) + server.commands.pop + end + yield server, client + ensure + client.logout rescue pp $! + client.disconnect if !client.disconnected? + end + ensure + server&.shutdown + end + end + +end From dba95ecbaa4def3eba4b7ca39e833d7036e2025a Mon Sep 17 00:00:00 2001 From: nick evans Date: Sat, 22 Jul 2023 13:20:49 -0400 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=93=9A=20Reorganize=20Net::IMAP=20cla?= =?UTF-8?q?ss=20rdoc=20yet=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This update started as a cleanup of the capabilities documentation, but along the way it became something bigger. Because yard doesn't honor the `#--` and `#++` delimiters, most of the TODO comments have been either removed or converted into readable documentation of the missing features. --- lib/net/imap.rb | 364 ++++++++++++++-------------------------------- lib/net/todo.rdoc | 70 +++++++++ 2 files changed, 182 insertions(+), 252 deletions(-) create mode 100644 lib/net/todo.rdoc diff --git a/lib/net/imap.rb b/lib/net/imap.rb index fb28f76a..81f6562d 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -25,10 +25,8 @@ module Net # Net::IMAP implements Internet Message Access Protocol (\IMAP) client # functionality. The protocol is described in - # [IMAP4rev1[https://tools.ietf.org/html/rfc3501]]. - #-- - # TODO: and [IMAP4rev2[https://tools.ietf.org/html/rfc9051]]. - #++ + # [IMAP4rev1[https://tools.ietf.org/html/rfc3501]] and + # [IMAP4rev2[https://tools.ietf.org/html/rfc9051]]. # # == \IMAP Overview # @@ -77,18 +75,9 @@ module Net # UIDs have to be reassigned. An \IMAP client thus cannot # rearrange message orders. # - # === Server capabilities and protocol extensions + # === Examples of Usage # - # 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. - # - # See the #capability method for more information. - # - # == Examples of Usage - # - # === List sender and subject of all recent messages in the default mailbox + # ==== List sender and subject of all recent messages in the default mailbox # # imap = Net::IMAP.new('mail.example.com') # imap.authenticate('LOGIN', 'joe_user', 'joes_password') @@ -98,7 +87,7 @@ module Net # puts "#{envelope.from[0].name}: \t#{envelope.subject}" # end # - # === Move all messages from April 2003 from "Mail/sent-mail" to "Mail/sent-apr03" + # ==== Move all messages from April 2003 from "Mail/sent-mail" to "Mail/sent-apr03" # # imap = Net::IMAP.new('mail.example.com') # imap.authenticate('LOGIN', 'joe_user', 'joes_password') @@ -112,6 +101,15 @@ module Net # end # imap.expunge # + # == 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. + # + # See the #capability method for more information. + # # == Thread Safety # # Net::IMAP supports concurrent threads. For example, @@ -173,24 +171,36 @@ module Net # == What's here? # # * {Connection control}[rdoc-ref:Net::IMAP@Connection+control+methods] - # * {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] - # * {...for the "authenticated" state}[rdoc-ref:Net::IMAP@IMAP+commands+for+the+-22Authenticated-22+state] - # * {...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] + # * {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands] + # * {for any state}[rdoc-ref:Net::IMAP@Any+state] + # * {for the "not authenticated" state}[rdoc-ref:Net::IMAP@Not+Authenticated+state] + # * {for the "authenticated" state}[rdoc-ref:Net::IMAP@Authenticated+state] + # * {for the "selected" state}[rdoc-ref:Net::IMAP@Selected+state] + # * {for the "logout" state}[rdoc-ref:Net::IMAP@Logout+state] + # * {IMAP extension support}[rdoc-ref:Net::IMAP@IMAP+extension+support] # # === Connection control methods # - # - Net::IMAP.new: A new client connects immediately and waits for a - # successful server greeting before returning the new client object. + # - Net::IMAP.new: Creates a new \IMAP client which connects immediately and + # waits for a successful server greeting before the method returns. # - #starttls: Asks the server to upgrade a clear-text connection to use TLS. # - #logout: Tells the server to end the session. Enters the "_logout_" state. # - #disconnect: Disconnects the connection (without sending #logout first). # - #disconnected?: True if the connection has been closed. # + # === 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 @@ -200,30 +210,12 @@ module Net # [NAMESPACE[https://tools.ietf.org/html/rfc2342]], # [UNSELECT[https://tools.ietf.org/html/rfc3691]], # [ENABLE[https://tools.ietf.org/html/rfc5161]], - #-- - # TODO: [LIST-EXTENDED[https://tools.ietf.org/html/rfc5258]], - # TODO: [LIST-STATUS[https://tools.ietf.org/html/rfc5819]], - #++ # [MOVE[https://tools.ietf.org/html/rfc6851]]. # These extensions are widely supported by modern IMAP4rev1 servers and have # all been integrated into [IMAP4rev2[https://tools.ietf.org/html/rfc9051]]. - # Note: Net::IMAP doesn't fully support IMAP4rev2 yet. - # - #-- - # TODO: When IMAP4rev2 is supported, add the following to the each of the - # appropriate commands below. - # Note:: CHECK has been removed from IMAP4rev2. - # Note:: LSUB is obsoleted by +LIST-EXTENDED and has been removed from IMAP4rev2. - # Some arguments require the +LIST-EXTENDED+ or +IMAP4rev2+ capability. - # Requires either the +ENABLE+ or +IMAP4rev2+ capability. - # Requires either the +NAMESPACE+ or +IMAP4rev2+ capability. - # Requires either the +IDLE+ or +IMAP4rev2+ capability. - # Requires either the +UNSELECT+ or +IMAP4rev2+ capability. - # Requires either the +UIDPLUS+ or +IMAP4rev2+ capability. - # Requires either the +MOVE+ or +IMAP4rev2+ capability. - #++ - # - # ==== \IMAP commands for any state + # *NOTE:* Net::IMAP doesn't support IMAP4rev2 yet. + # + # ==== Any state # # - #capability: Returns the server's capabilities as an array of strings. # @@ -232,34 +224,33 @@ module Net # - #noop: Allows the server to send unsolicited untagged #responses. # - #logout: Tells the server to end the session. Enters the "_logout_" state. # - # ==== \IMAP commands for the "Not Authenticated" state + # ==== Not Authenticated state # - # In addition to the universal commands, the following commands are valid in - # the "not authenticated" state: + # In addition to the commands for any state, the following commands are valid + # in the "not authenticated" state: # # - #starttls: Upgrades a clear-text connection to use TLS. # # Requires the +STARTTLS+ capability. - # - #authenticate: Identifies the client to the server using a {SASL - # mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]. - # Enters the "_authenticated_" state. + # - #authenticate: Identifies the client to the server using the given + # {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml] + # and credentials. Enters the "_authenticated_" state. # - # Requires the AUTH=#{mechanism} capability for the chosen - # mechanism. + # The server should list "AUTH=#{mechanism}" capabilities for + # supported mechanisms. # - #login: Identifies the client to the server using a plain text password. # Using #authenticate is generally preferred. Enters the "_authenticated_" # state. # # The +LOGINDISABLED+ capability must NOT be listed. # - # ==== \IMAP commands for the "Authenticated" state + # ==== Authenticated state # - # In addition to the universal commands, the following commands are valid in - # the "_authenticated_" state: + # In addition to the commands for any state, the following commands are valid + # in the "_authenticated_" state: # # - #enable: Enables backwards incompatible server extensions. - # - # Requires the +ENABLE+ capability. + # Requires the +ENABLE+ or +IMAP4rev2+ capability. # - #select: Open a mailbox and enter the "_selected_" state. # - #examine: Open a mailbox read-only, and enter the "_selected_" state. # - #create: Creates a new mailbox. @@ -269,37 +260,31 @@ module Net # - #unsubscribe: Removes a mailbox from the "subscribed" set. # - #list: Returns names and attributes of mailboxes matching a given pattern. # - #namespace: Returns mailbox namespaces, with path prefixes and delimiters. - # - # Requires the +NAMESPACE+ capability. + # Requires the +NAMESPACE+ or +IMAP4rev2+ capability. # - #status: Returns mailbox information, e.g. message count, unseen message # count, +UIDVALIDITY+ and +UIDNEXT+. # - #append: Appends a message to the end of a mailbox. # - #idle: Allows the server to send updates to the client, without the client # needing to poll using #noop. + # Requires the +IDLE+ or +IMAP4rev2+ capability. + # - *Obsolete* #lsub: Replaced by LIST-EXTENDED and removed from + # +IMAP4rev2+. Lists mailboxes in the "subscribed" set. # - # Requires the +IDLE+ capability. - # - #lsub: Lists mailboxes the user has declared "active" or "subscribed". - #-- - # Replaced by LIST-EXTENDED and removed from - # +IMAP4rev2+. However, Net::IMAP hasn't implemented - # LIST-EXTENDED _yet_. - #++ + # *Note:* Net::IMAP hasn't implemented LIST-EXTENDED yet. # - # ==== \IMAP commands for the "Selected" state + # ==== Selected state # - # In addition to the universal commands and the "authenticated" commands, the - # following commands are valid in the "_selected_" state: + # In addition to the commands for any state and the "_authenticated_" + # commands, the following commands are valid in the "_selected_" state: # # - #close: Closes the mailbox and returns to the "_authenticated_" state, # expunging deleted messages, unless the mailbox was opened as read-only. # - #unselect: Closes the mailbox and returns to the "_authenticated_" state, # without expunging any messages. - # - # Requires the +UNSELECT+ capability. + # Requires the +UNSELECT+ or +IMAP4rev2+ capability. # - #expunge: Permanently removes messages which have the Deleted flag set. - # - #uid_expunge: Restricts #expunge to only remove the specified UIDs. - # - # Requires the +UIDPLUS+ capability. + # - #uid_expunge: Restricts expunge to only remove the specified UIDs. + # Requires the +UIDPLUS+ or +IMAP4rev2+ capability. # - #search, #uid_search: Returns sequence numbers or UIDs of messages that # match the given searching criteria. # - #fetch, #uid_fetch: Returns data associated with a set of messages, @@ -309,44 +294,35 @@ module Net # specified destination mailbox. # - #move, #uid_move: Moves the specified messages to the end of the # specified destination mailbox, expunging them from the current mailbox. + # Requires the +MOVE+ or +IMAP4rev2+ capability. + # - #check: *Obsolete:* removed from +IMAP4rev2+. + # Can be replaced with #noop or #idle. # - # Requires the +MOVE+ capability. - # - #check: Mostly obsolete. Can be replaced with #noop or #idle. - #-- - # Removed from IMAP4rev2. - #++ + # ==== Logout state # - # ==== \IMAP commands for the "Logout" state - # - # No \IMAP commands are valid in the +logout+ state. If the socket is still + # No \IMAP commands are valid in the "_logout_" state. If the socket is still # open, Net::IMAP will close it after receiving server confirmation. # Exceptions will be raised by \IMAP commands that have already started and # are waiting for a response, as well as any that are called after logout. # - # === Supported \IMAP extensions + # === \IMAP extension support # # ==== RFC9051: +IMAP4rev2+ # - # Although IMAP4rev2[https://tools.ietf.org/html/rfc9051] is not supported - # yet, Net::IMAP supports several extensions that have been folded into - # it: +ENABLE+, +IDLE+, +MOVE+, +NAMESPACE+, +UIDPLUS+, and +UNSELECT+. - #-- - # TODO: RFC4466, ABNF extensions (automatic support for other extensions) - # TODO: +ESEARCH+, ExtendedSearchData - # TODO: +SEARCHRES+, - # TODO: +SASL-IR+, - # TODO: +LIST-EXTENDED+, - # TODO: +LIST-STATUS+, - # TODO: +LITERAL-+, - # TODO: +BINARY+ (only the FETCH side) - # TODO: +SPECIAL-USE+ - # implicitly supported, but we can do better: Response codes: RFC5530, etc - # implicitly supported, but we can do better: STATUS=SIZE - # implicitly supported, but we can do better: STATUS DELETED - #++ - # Commands for these extensions are included with the {Core IMAP - # commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands], above. Other supported - # extensons are listed below. + # Although IMAP4rev2[https://tools.ietf.org/html/rfc9051] is not supported + # yet, Net::IMAP supports several extensions that have been folded into it: + # +ENABLE+, +IDLE+, +MOVE+, +NAMESPACE+, +UIDPLUS+, and +UNSELECT+. Commands + # for these extensions are listed with the + # {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands], above. + # + # >>> + # The following are folded into +IMAP4rev2+ but are currently + # unsupported or incompletely supported by Net::IMAP: RFC4466 + # extensions, +ESEARCH+, +SEARCHRES+, +SASL-IR+, +LIST-EXTENDED+, + # +LIST-STATUS+, +LITERAL-+, +BINARY+ fetch, and +SPECIAL-USE+. The + # following extensions are implicitly supported, but will be updated with + # more direct support: RFC5530 response codes, STATUS=SIZE, and + # STATUS=DELETED. # # ==== RFC2087: +QUOTA+ # - #getquota: returns the resource usage and limits for a quota root @@ -355,98 +331,44 @@ module Net # - #setquota: sets the resource limits for a given quota root. # # ==== RFC2177: +IDLE+ - # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051], so it is also - # listed with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands]. + # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051] and also included + # above with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands]. # - #idle: Allows the server to send updates to the client, without the client # needing to poll using #noop. # # ==== RFC2342: +NAMESPACE+ - # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051], so it is also - # listed with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands]. + # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051] and also included + # above with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands]. # - #namespace: Returns mailbox namespaces, with path prefixes and delimiters. # # ==== RFC2971: +ID+ # - #id: exchanges client and server implementation information. # - #-- - # ==== RFC3502: +MULTIAPPEND+ - # TODO... - #++ - # - #-- - # ==== RFC3516: +BINARY+ - # TODO... - #++ - # # ==== RFC3691: +UNSELECT+ - # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051], so it is also - # listed with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands]. + # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051] and also included + # above with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands]. # - #unselect: Closes the mailbox and returns to the "_authenticated_" state, # without expunging any messages. # # ==== RFC4314: +ACL+ # - #getacl: lists the authenticated user's access rights to a mailbox. # - #setacl: sets the access rights for a user on a mailbox - #-- - # TODO: #deleteacl, #listrights, #myrights - #++ - # - *_Note:_* +DELETEACL+, +LISTRIGHTS+, and +MYRIGHTS+ are not supported yet. + # >>> + # *NOTE:* +DELETEACL+, +LISTRIGHTS+, and +MYRIGHTS+ are not supported yet. # # ==== RFC4315: +UIDPLUS+ - # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051], so it is also - # listed with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands]. + # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051] and also included + # above with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands]. # - #uid_expunge: Restricts #expunge to only remove the specified UIDs. # - Updates #select, #examine with the +UIDNOTSTICKY+ ResponseCode # - Updates #append with the +APPENDUID+ ResponseCode # - Updates #copy, #move with the +COPYUID+ ResponseCode # - #-- - # ==== RFC4466: Collected Extensions to IMAP4 ABNF - # TODO... - # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051], this RFC updates - # the protocol to enable new optional parameters to many commands: #select, - # #examine, #create, #rename, #fetch, #uid_fetch, #store, #uid_store, #search, - # #uid_search, and #append. However, specific parameters are not defined. - # Extensions to these commands use this syntax whenever possible. Net::IMAP - # may be partially compatible with extensions to these commands, even without - # any explicit support. - #++ - # - #-- - # ==== RFC4731 +ESEARCH+ - # TODO... - # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051]. - # - Updates #search, #uid_search to accept result options: +MIN+, +MAX+, - # +ALL+, +COUNT+, and to return ExtendedSearchData. - #++ - # - #-- - # ==== RFC4959: +SASL-IR+ - # TODO... - # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051]. - # - Updates #authenticate to reduce round-trips for supporting mechanisms. - #++ - # - #-- - # ==== RFC4978: COMPRESS=DEFLATE - # TODO... - #++ - # # ==== RFC5161: +ENABLE+ - # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051], so it is also - # listed with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands]. + # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051] and also included + # above with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands]. # - #enable: Enables backwards incompatible server extensions. # - #-- - # ==== RFC5182 +SEARCHRES+ - # TODO... - # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051]. - # - Updates #search, #uid_search with the +SAVE+ result option. - # - Updates #copy, #uid_copy, #fetch, #uid_fetch, #move, #uid_move, #search, - # #uid_search, #store, #uid_store, and #uid_expunge with ability to - # reference the saved result of a previous #search or #uid_search command. - #++ - # # ==== RFC5256: +SORT+ # - #sort, #uid_sort: An alternate version of #search or #uid_search which # sorts the results by specified keys. @@ -455,37 +377,12 @@ module Net # which arranges the results into ordered groups or threads according to a # chosen algorithm. # - #-- - # ==== RFC5258 +LIST-EXTENDED+ - # TODO... - # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051], this updates the - # protocol with new optional parameters to the #list command, adding a few of - # its own. Net::IMAP may be forward-compatible with future #list extensions, - # even without any explicit support. - # - Updates #list to accept selection options: +SUBSCRIBED+, +REMOTE+, and - # +RECURSIVEMATCH+, and return options: +SUBSCRIBED+ and +CHILDREN+. - #++ - # - #-- - # ==== RFC5819 +LIST-STATUS+ - # TODO... - # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051]. - # - Updates #list with +STATUS+ return option. - #++ - # # ==== +XLIST+ (non-standard, deprecated) # - #xlist: replaced by +SPECIAL-USE+ attributes in #list responses. # - #-- - # ==== RFC6154 +SPECIAL-USE+ - # TODO... - # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051]. - # - Updates #list with the +SPECIAL-USE+ selection and return options. - #++ - # # ==== RFC6851: +MOVE+ - # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051], so it is also - # listed with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands]. + # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051] and also included + # above with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands]. # - #move, #uid_move: Moves the specified messages to the end of the # specified destination mailbox, expunging them from the current mailbox. # @@ -493,36 +390,7 @@ module Net # # - See #enable for information about support for UTF-8 string encoding. # - #-- - # ==== RFC7888: LITERAL+, +LITERAL-+ - # TODO... - # ==== RFC7162: +QRESYNC+ - # TODO... - # ==== RFC7162: +CONDSTORE+ - # TODO... - # ==== RFC8474: +OBJECTID+ - # TODO... - # ==== RFC9208: +QUOTA+ - # 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 - #-- - # TODO: Consider moving references list to REFERENCES.md or REFERENCES.rdoc. - #++ # # [{IMAP4rev1}[https://www.rfc-editor.org/rfc/rfc3501.html]]:: # Crispin, M., "INTERNET MESSAGE ACCESS PROTOCOL - \VERSION 4rev1", @@ -623,27 +491,21 @@ module Net # RFC 1864, DOI 10.17487/RFC1864, October 1995, # . # - #-- - # TODO: Document IMAP keywords. + # [RFC3503[https://tools.ietf.org/html/rfc3503]]:: + # Melnikov, A., "Message Disposition Notification (MDN) + # profile for Internet Message Access Protocol (IMAP)", + # RFC 3503, DOI 10.17487/RFC3503, March 2003, + # . # - # [RFC3503[https://tools.ietf.org/html/rfc3503]] - # Melnikov, A., "Message Disposition Notification (MDN) - # profile for Internet Message Access Protocol (IMAP)", - # RFC 3503, DOI 10.17487/RFC3503, March 2003, - # . - #++ + # === \IMAP Extensions # - # === Supported \IMAP Extensions - # - # [QUOTA[https://tools.ietf.org/html/rfc2087]]:: - # Myers, J., "IMAP4 QUOTA extension", RFC 2087, DOI 10.17487/RFC2087, - # January 1997, . - #-- - # TODO: test compatibility with updated QUOTA extension: # [QUOTA[https://tools.ietf.org/html/rfc9208]]:: # Melnikov, A., "IMAP QUOTA Extension", RFC 9208, DOI 10.17487/RFC9208, # March 2022, . - #++ + # + # Note: obsoletes + # RFC-2087[https://tools.ietf.org/html/rfc2087] (January 1997). + # Net::IMAP does not fully support the RFC9208 updates yet. # [IDLE[https://tools.ietf.org/html/rfc2177]]:: # Leiba, B., "IMAP4 IDLE command", RFC 2177, DOI 10.17487/RFC2177, # June 1997, . @@ -683,26 +545,24 @@ module Net # . # # === IANA registries - # # * {IMAP Capabilities}[http://www.iana.org/assignments/imap4-capabilities] # * {IMAP Response Codes}[https://www.iana.org/assignments/imap-response-codes/imap-response-codes.xhtml] # * {IMAP Mailbox Name Attributes}[https://www.iana.org/assignments/imap-mailbox-name-attributes/imap-mailbox-name-attributes.xhtml] # * {IMAP and JMAP Keywords}[https://www.iana.org/assignments/imap-jmap-keywords/imap-jmap-keywords.xhtml] # * {IMAP Threading Algorithms}[https://www.iana.org/assignments/imap-threading-algorithms/imap-threading-algorithms.xhtml] - #-- - # * {IMAP Quota Resource Types}[http://www.iana.org/assignments/imap4-capabilities#imap-capabilities-2] - # * [{LIST-EXTENDED options and responses}[https://www.iana.org/assignments/imap-list-extended/imap-list-extended.xhtml] - # * {IMAP METADATA Server Entry and Mailbox Entry Registries}[https://www.iana.org/assignments/imap-metadata/imap-metadata.xhtml] - # * {IMAP ANNOTATE Extension Entries and Attributes}[https://www.iana.org/assignments/imap-annotate-extension/imap-annotate-extension.xhtml] - # * {IMAP URLAUTH Access Identifiers and Prefixes}[https://www.iana.org/assignments/urlauth-access-ids/urlauth-access-ids.xhtml] - # * {IMAP URLAUTH Authorization Mechanism Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml] - #++ # * {SASL Mechanisms and SASL SCRAM Family Mechanisms}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml] # * {Service Name and Transport Protocol Port Number Registry}[https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xml]: # +imap+: tcp/143, +imaps+: tcp/993 # * {GSSAPI/Kerberos/SASL Service Names}[https://www.iana.org/assignments/gssapi-service-names/gssapi-service-names.xhtml]: # +imap+ # * {Character sets}[https://www.iana.org/assignments/character-sets/character-sets.xhtml] + # ===== For currently unsupported features: + # * {IMAP Quota Resource Types}[http://www.iana.org/assignments/imap4-capabilities#imap-capabilities-2] + # * {LIST-EXTENDED options and responses}[https://www.iana.org/assignments/imap-list-extended/imap-list-extended.xhtml] + # * {IMAP METADATA Server Entry and Mailbox Entry Registries}[https://www.iana.org/assignments/imap-metadata/imap-metadata.xhtml] + # * {IMAP ANNOTATE Extension Entries and Attributes}[https://www.iana.org/assignments/imap-annotate-extension/imap-annotate-extension.xhtml] + # * {IMAP URLAUTH Access Identifiers and Prefixes}[https://www.iana.org/assignments/urlauth-access-ids/urlauth-access-ids.xhtml] + # * {IMAP URLAUTH Authorization Mechanism Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml] # class IMAP < Protocol VERSION = "0.3.4" diff --git a/lib/net/todo.rdoc b/lib/net/todo.rdoc new file mode 100644 index 00000000..dc7cbcf8 --- /dev/null +++ b/lib/net/todo.rdoc @@ -0,0 +1,70 @@ +Moved from lib/net/imap.rb + +==== RFC3502: +MULTIAPPEND+ +TODO... + +==== RFC3516: +BINARY+ +TODO... + +==== RFC4466: Collected Extensions to IMAP4 ABNF +TODO... +Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051], this RFC updates +the protocol to enable new optional parameters to many commands: #select, +#examine, #create, #rename, #fetch, #uid_fetch, #store, #uid_store, #search, +#uid_search, and #append. However, specific parameters are not defined. +Extensions to these commands use this syntax whenever possible. Net::IMAP +may be partially compatible with extensions to these commands, even without +any explicit support. + +==== RFC4731 +ESEARCH+ +TODO... +Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051]. +- Updates #search, #uid_search to accept result options: +MIN+, +MAX+, + +ALL+, +COUNT+, and to return ExtendedSearchData. + +==== RFC4959: +SASL-IR+ +TODO... +Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051]. +- Updates #authenticate to reduce round-trips for supporting mechanisms. + +==== RFC4978: COMPRESS=DEFLATE +TODO... + +==== RFC5182 +SEARCHRES+ +TODO... +Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051]. +- Updates #search, #uid_search with the +SAVE+ result option. +- Updates #copy, #uid_copy, #fetch, #uid_fetch, #move, #uid_move, #search, + #uid_search, #store, #uid_store, and #uid_expunge with ability to + reference the saved result of a previous #search or #uid_search command. + +==== RFC5258 +LIST-EXTENDED+ +TODO... +Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051], this updates the +protocol with new optional parameters to the #list command, adding a few of +its own. Net::IMAP may be forward-compatible with future #list extensions, +even without any explicit support. +- Updates #list to accept selection options: +SUBSCRIBED+, +REMOTE+, and + +RECURSIVEMATCH+, and return options: +SUBSCRIBED+ and +CHILDREN+. + +==== RFC5819 +LIST-STATUS+ +TODO... +Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051]. +- Updates #list with +STATUS+ return option. + +==== RFC6154 +SPECIAL-USE+ +TODO... +Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051]. +- Updates #list with the +SPECIAL-USE+ selection and return options. + +==== RFC7888: LITERAL+, +LITERAL-+ +TODO... +==== RFC7162: +QRESYNC+ +TODO... +==== RFC7162: +CONDSTORE+ +TODO... +==== RFC8474: +OBJECTID+ +TODO... +==== RFC9208: +QUOTA+ +TODO... + From b0f8b9ea40d9a941d494eceb92938d5895210897 Mon Sep 17 00:00:00 2001 From: nicholas evans Date: Sun, 16 Jul 2023 08:18:20 -0400 Subject: [PATCH 4/4] =?UTF-8?q?=E2=9C=A8=20Add=20cached=20#capabilities,?= =?UTF-8?q?=20#capable=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. --- lib/net/imap.rb | 354 +++++++++++++++++------- test/net/imap/test_imap_capabilities.rb | 296 ++++++++++++++++++++ 2 files changed, 556 insertions(+), 94 deletions(-) create mode 100644 test/net/imap/test_imap_capabilities.rb diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 81f6562d..bf807643 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -103,12 +103,83 @@ 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 + # 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+, AUTH=PLAIN, + # 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. # # == Thread Safety # @@ -171,6 +242,7 @@ 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@Any+state] @@ -189,6 +261,23 @@ 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 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. + # + # 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 @@ -219,8 +308,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. # @@ -662,64 +751,132 @@ def disconnected? 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. # # >>> - # *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 server's advertised capabilities. Users of this class + # must check that the server is #capable? of extension commands or command + # arguments before sending them. + # + # 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: #auth_capable?, #capabilities, #capability, #enable # - # Related: #enable + # ===== Caching +CAPABILITY+ responses + # + # 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. # - # ===== Basic IMAP4rev1 capabilities + 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 AUTH= prefix. # - # 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. + # This may be different when the connection is cleartext or using TLS. Most + # servers will drop all AUTH= mechanisms from #capabilities after + # the connection has authenticated. # - # ===== Using IMAP4rev1 extensions + # Related: #auth_capable?, #capabilities # - # 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. + # ===== Example # - # 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. + # 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 # => [] # - # ===== Caching +CAPABILITY+ responses + 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 "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 + + # Returns whether capabilities have been cached. When true, #capable? and + # #capabilities don't require sending a #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. + + # 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. + # + # >>> + # *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. + # + # Capability requirements—other than +IMAP4rev1+—are listed in the + # documentation for each command method. # - # 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. + # 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 @@ -730,8 +887,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, @@ -791,21 +947,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| @@ -816,6 +968,8 @@ 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 @@ -870,18 +1024,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... @@ -890,18 +1043,16 @@ 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" - # 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 @@ -917,6 +1068,9 @@ def authenticate(mechanism, ...) 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] @@ -932,8 +1086,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. @@ -942,6 +1096,7 @@ def authenticate(mechanism, ...) # 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] @@ -1121,8 +1276,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." @@ -1459,7 +1613,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: # @@ -1763,7 +1917,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 # @@ -1802,7 +1956,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"). @@ -2107,7 +2261,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 = capabilities_from_resp_code @greeting if @greeting.name == "BYE" raise ByeResponseError, @greeting end @@ -2171,8 +2326,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) @@ -2250,20 +2404,32 @@ 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) + # 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| diff --git a/test/net/imap/test_imap_capabilities.rb b/test/net/imap/test_imap_capabilities.rb new file mode 100644 index 00000000..31f4d0ff --- /dev/null +++ b/test/net/imap/test_imap_capabilities.rb @@ -0,0 +1,296 @@ +# frozen_string_literal: true + +require "net/imap" +require "test/unit" +require_relative "fake_server" + +class IMAPCapabilitiesTest < Test::Unit::TestCase + + include Net::IMAP::FakeServer::TestHelper + + def setup + @do_not_reverse_lookup = Socket.do_not_reverse_lookup + Socket.do_not_reverse_lookup = true + @threads = [] + end + + def teardown + if !@threads.empty? + assert_join_threads(@threads) + end + ensure + Socket.do_not_reverse_lookup = @do_not_reverse_lookup + 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 + # only one CAPABILITY command was sent + 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 + # only one CAPABILITY command was sent + 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 + # only one CAPABILITY command was sent + assert_equal "CAPABILITY", server.commands.pop.name + assert_empty server.commands + end + end + + test "#auth_mechanisms reports cached capabilities with 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_equal(%w[PLAIN SCRAM-SHA-1 SCRAM-SHA-256 XOAUTH2 OAUTHBEARER], + imap.auth_mechanisms) + end + # only one CAPABILITY command was sent + 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 "#capability 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 + assert_empty imap.auth_mechanisms + refute imap.auth_capable? "plain" + refute imap.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_equal %w[PLAIN], imap.auth_mechanisms + assert imap.capable? :IMAP4rev1 + 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") + assert imap.auth_capable?("PLAIN") + + imap.authenticate("PLAIN", "test_user", "test-password") + assert_equal "AUTHENTICATE", server.commands.pop.name + refute imap.capabilities_cached? + + assert imap.capable? :IMAP4rev1 + refute imap.auth_capable?("PLAIN") + assert_empty imap.auth_mechanisms + assert_equal "CAPABILITY", server.commands.pop.name + assert_empty server.commands + end + end + + # TODO: should we warn about this? + 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 + +end