From 800edb8e09fb09bc839d35c15cce21a82190c2aa Mon Sep 17 00:00:00 2001
From: nick evans <nicholas.evans@gmail.com>
Date: Sun, 1 Jan 2023 14:32:45 -0500
Subject: [PATCH] =?UTF-8?q?=F0=9F=92=A5=20Update=20responses=20methods=20f?=
 =?UTF-8?q?or=20thread-safety?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Calling `#responses` with no block is deprecated, as it can't be made
thread-safe and backward-compatible.  However, the deprecation warning
is commented out for now, until after I can check the test suites for
the rails and mail gems and submit PRs there if necessary.

For convenience, an optional `type` paramater has been added to
`#responses`, to yield only the array for that type of response.

Added `#clear_responses` as a convenience method for the common "read
and delete" pattern, and to provide a thread-safe responses
method that doesn't require a block.

The response handlers methods are now synchronized, but
`#response_handlers` now returns a frozen clone to alert users who had
manipulated it directly.  Hopefully very few are affected, but this
change is backwards incompatible.  N.b: there currently is no API for
inserting a response_handler into a particular position in the array.

Also, improve the yields_in_test_server_thread test helper: it no longer
reads `last_tag` from the block result, it sets a short `IO#timeout` in
ruby 3.2+, it uses a slightly longer timeout for overall test
completion, and it defines `sock.getcmd` so the tests can read more
intuitively than using a proc passed into the block.

Reference: https://github.com/ruby/net-imap/commit/800edb8e09fb09bc839d35c15cce21a82190c2aa
Conflict: (1) path remapped lib/net/imap.rb -> .bundle/gems/net-imap-0.3.8/lib/net/imap.rb (bundled gem v0.3.8 in ruby-3.2.8)
(2) duplicate rdoc line "# Related: #remove_response_handler, #response_handlers" above #add_response_handler was not re-added (already present in v0.3.8)
(3) test case hunk (test/net/imap/test_imap.rb) not modified (skipped per adaptation rule)
---
 .bundle/gems/net-imap-0.3.8/lib/net/imap.rb | 140 ++++++++++++++++----
 1 file changed, 115 insertions(+), 25 deletions(-)

diff --git a/.bundle/gems/net-imap-0.3.8/lib/net/imap.rb b/.bundle/gems/net-imap-0.3.8/lib/net/imap.rb
index bf857f79dd10..0e5910cb4a69 100644
--- a/.bundle/gems/net-imap-0.3.8/lib/net/imap.rb
+++ b/.bundle/gems/net-imap-0.3.8/lib/net/imap.rb
@@ -554,10 +554,12 @@ module Net
   #
   # - #greeting: The server's initial untagged response, which can indicate a
   #   pre-authenticated connection.
-  # - #responses: A hash with arrays of unhandled <em>non-+nil+</em>
-  #   UntaggedResponse and ResponseCode +#data+, keyed by +#name+.
+  # - #responses: Yields unhandled UntaggedResponse#data and <em>non-+nil+</em>
+  #   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.
   #
   #
@@ -765,22 +767,6 @@ module Net
     # Returns the initial greeting the server, an UntaggedResponse.
     attr_reader :greeting
 
-    # Returns a hash with arrays of unhandled <em>non-+nil+</em>
-    # UntaggedResponse#data keyed by UntaggedResponse#name, and
-    # ResponseCode#data keyed by ResponseCode#name.
-    #
-    # For example:
-    #
-    #   imap.select("inbox")
-    #   p imap.responses["EXISTS"][-1]
-    #   #=> 2
-    #   p imap.responses["UIDVALIDITY"][-1]
-    #   #=> 968263756
-    attr_reader :responses
-
-    # Returns all response handlers.
-    attr_reader :response_handlers
-
     # Seconds to wait until a connection is opened.
     # If the IMAP object cannot open a connection within this time,
     # it raises a Net::OpenTimeout exception. The default value is 30 seconds.
@@ -1188,11 +1174,11 @@ module Net
     # to select a +mailbox+ so that messages in the +mailbox+ can be accessed.
     #
     # After you have selected a mailbox, you may retrieve the number of items in
-    # that mailbox from <tt>imap.responses["EXISTS"][-1]</tt>, and the number of
-    # recent messages from <tt>imap.responses["RECENT"][-1]</tt>.  Note that
-    # these values can change if new messages arrive during a session or when
-    # existing messages are expunged; see #add_response_handler for a way to
-    # detect these events.
+    # that mailbox from <tt>imap.responses("EXISTS", &:last)</tt>, and the
+    # number of recent messages from <tt>imap.responses("RECENT", &:last)</tt>.
+    # Note that these values can change if new messages arrive during a session
+    # or when existing messages are expunged; see #add_response_handler for a
+    # way to detect these events.
     #
     # A Net::IMAP::NoResponseError is raised if the mailbox does not
     # exist or is for some reason non-selectable.
@@ -2068,6 +2054,104 @@ module Net
       end
     end
 
+    # :call-seq:
+    #   responses       {|hash|  ...} -> block result
+    #   responses(type) {|array| ...} -> block result
+    #
+    # Yields unhandled responses and returns the result of the block.
+    #
+    # Unhandled responses are stored in a hash, with arrays of
+    # <em>non-+nil+</em> UntaggedResponse#data keyed by UntaggedResponse#name
+    # and ResponseCode#data keyed by ResponseCode#name.  Call without +type+ to
+    # yield the entire responses hash.  Call with +type+ to yield only the array
+    # of responses for that type.
+    #
+    # For example:
+    #
+    #   imap.select("inbox")
+    #   p imap.responses("EXISTS", &:last)
+    #   #=> 2
+    #   p imap.responses("UIDVALIDITY", &:last)
+    #   #=> 968263756
+    #
+    # >>>
+    #   *Note:* Access to the responses hash is synchronized for thread-safety.
+    #   The receiver thread and response_handlers cannot process new responses
+    #   until the block completes.  Accessing either the response hash or its
+    #   response type arrays outside of the block is unsafe.
+    #
+    #   Calling without a block is unsafe and deprecated.  Future releases will
+    #   raise ArgumentError unless a block is given.
+    #
+    # Previously unhandled responses are automatically cleared before entering a
+    # mailbox with #select or #examine.  Long-lived connections can receive many
+    # unhandled server responses, which must be pruned or they will continually
+    # consume more memory.  Update or clear the responses hash or arrays inside
+    # the block, or use #clear_responses.
+    #
+    # Only non-+nil+ data is stored.  Many important response codes have no data
+    # of their own, but are used as "tags" on the ResponseText object they are
+    # attached to.  ResponseText will be accessible by its response types:
+    # "+OK+", "+NO+", "+BAD+", "+BYE+", or "+PREAUTH+".
+    #
+    # TaggedResponse#data is not saved to #responses, nor is any
+    # ResponseCode#data on tagged responses.  Although some command methods do
+    # return the TaggedResponse directly, #add_response_handler must be used to
+    # handle all response codes.
+    #
+    # Related: #clear_responses, #response_handlers, #greeting
+    def responses(type = nil)
+      if block_given?
+        synchronize { yield(type ? @responses[type.to_s.upcase] : @responses) }
+      elsif type
+        raise ArgumentError, "Pass a block or use #clear_responses"
+      else
+        # warn("DEPRECATED: pass a block or use #clear_responses", uplevel: 1)
+        @responses
+      end
+    end
+
+    # :call-seq:
+    #   clear_responses       -> hash
+    #   clear_responses(type) -> array
+    #
+    # Clears and returns the unhandled #responses hash or the unhandled
+    # responses array for a single response +type+.
+    #
+    # Clearing responses is synchronized with other threads.  The lock is
+    # released before returning.
+    #
+    # Related: #responses, #response_handlers
+    def clear_responses(type = nil)
+      synchronize {
+        if type
+          @responses.delete(type) || []
+        else
+          @responses.dup.transform_values(&:freeze)
+            .tap { _1.default = [].freeze }
+            .tap { @responses.clear }
+        end
+      }
+        .freeze
+    end
+
+    # Returns all response handlers, including those that are added internally
+    # by commands.  Each response handler will be called with every new
+    # UntaggedResponse, TaggedResponse, and ContinuationRequest.
+    #
+    # Response handlers are called with a mutex inside the receiver thread.  New
+    # responses cannot be processed and commands from other threads must wait
+    # until all response_handlers return.  An exception will shut-down the
+    # receiver thread and close the connection.
+    #
+    # For thread-safety, the returned array is a frozen copy of the internal
+    # array.
+    #
+    # Related: #add_response_handler, #remove_response_handler
+    def response_handlers
+      synchronize { @response_handlers.clone.freeze }
+    end
+
     # Adds a response handler. For example, to detect when
     # the server sends a new EXISTS response (which normally
     # indicates new messages being added to the mailbox),
@@ -2087,12 +2171,18 @@ module Net
     # Related: #remove_response_handler, #response_handlers
     def add_response_handler(handler = nil, &block)
       raise ArgumentError, "two Procs are passed" if handler && block
-      @response_handlers.push(block || handler)
+      synchronize do
+        @response_handlers.push(block || handler)
+      end
     end
 
     # Removes the response handler.
+    #
+    # Related: #add_response_handler, #response_handlers
     def remove_response_handler(handler)
-      @response_handlers.delete(handler)
+      synchronize do
+        @response_handlers.delete(handler)
+      end
     end
 
     private
-- 
2.43.0