@@ -43,10 +43,16 @@ module Net
4343 # To work on the messages within a mailbox, the client must
4444 # first select that mailbox, using either #select or #examine
4545 # (for read-only access). Once the client has successfully
46- # selected a mailbox, they enter the "_selected_" state, and that
46+ # selected a mailbox, they enter the +selected+ state, and that
4747 # mailbox becomes the _current_ mailbox, on which mail-item
4848 # related commands implicitly operate.
4949 #
50+ # === Connection state
51+ #
52+ # Once an IMAP connection is established, the connection is in one of four
53+ # states: <tt>not authenticated</tt>, +authenticated+, +selected+, and
54+ # +logout+. Most commands are valid only in certain states.
55+ #
5056 # === Sequence numbers and UIDs
5157 #
5258 # Messages have two sorts of identifiers: message sequence
@@ -199,6 +205,42 @@ module Net
199205 #
200206 # This script invokes the FETCH command and the SEARCH command concurrently.
201207 #
208+ # When running multiple commands, care must be taken to avoid ambiguity. For
209+ # example, SEARCH responses are ambiguous about which command they are
210+ # responding to, so search commands should not run simultaneously, unless the
211+ # server supports +ESEARCH+ {[RFC4731]}[https://rfc-editor.org/rfc/rfc4731] or
212+ # IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051]. See {RFC9051
213+ # §5.5}[https://www.rfc-editor.org/rfc/rfc9051.html#section-5.5] for
214+ # other examples of command sequences which should not be pipelined.
215+ #
216+ # == Unbounded memory use
217+ #
218+ # Net::IMAP reads server responses in a separate receiver thread per client.
219+ # Unhandled response data is saved to #responses, and response_handlers run
220+ # inside the receiver thread. See the list of methods for {handling server
221+ # responses}[rdoc-ref:Net::IMAP@Handling+server+responses], below.
222+ #
223+ # Because the receiver thread continuously reads and saves new responses, some
224+ # scenarios must be careful to avoid unbounded memory use:
225+ #
226+ # * Commands such as #list or #fetch can have an enormous number of responses.
227+ # * Commands such as #fetch can result in an enormous size per response.
228+ # * Long-lived connections will gradually accumulate unsolicited server
229+ # responses, especially +EXISTS+, +FETCH+, and +EXPUNGE+ responses.
230+ # * A buggy or untrusted server could send inappropriate responses, which
231+ # could be very numerous, very large, and very rapid.
232+ #
233+ # Use paginated or limited versions of commands whenever possible.
234+ #
235+ # Use Config#max_response_size to impose a limit on incoming server responses
236+ # as they are being read. <em>This is especially important for untrusted
237+ # servers.</em>
238+ #
239+ # Use #add_response_handler to handle responses after each one is received.
240+ # Use the +response_handlers+ argument to ::new to assign response handlers
241+ # before the receiver thread is started. Use #extract_responses,
242+ # #clear_responses, or #responses (with a block) to prune responses.
243+ #
202244 # == Errors
203245 #
204246 # An \IMAP server can send three different types of responses to indicate
@@ -260,8 +302,9 @@ module Net
260302 #
261303 # - Net::IMAP.new: Creates a new \IMAP client which connects immediately and
262304 # waits for a successful server greeting before the method returns.
305+ # - #connection_state: Returns the connection state.
263306 # - #starttls: Asks the server to upgrade a clear-text connection to use TLS.
264- # - #logout: Tells the server to end the session. Enters the "_logout_" state.
307+ # - #logout: Tells the server to end the session. Enters the +logout+ state.
265308 # - #disconnect: Disconnects the connection (without sending #logout first).
266309 # - #disconnected?: True if the connection has been closed.
267310 #
@@ -317,37 +360,36 @@ module Net
317360 # <em>In general, #capable? should be used rather than explicitly sending a
318361 # +CAPABILITY+ command to the server.</em>
319362 # - #noop: Allows the server to send unsolicited untagged #responses.
320- # - #logout: Tells the server to end the session. Enters the "_logout_" state.
363+ # - #logout: Tells the server to end the session. Enters the +logout+ state.
321364 #
322365 # ==== Not Authenticated state
323366 #
324367 # In addition to the commands for any state, the following commands are valid
325- # in the "<em>not authenticated</em>" state:
368+ # in the +not_authenticated+ state:
326369 #
327370 # - #starttls: Upgrades a clear-text connection to use TLS.
328371 #
329372 # <em>Requires the +STARTTLS+ capability.</em>
330373 # - #authenticate: Identifies the client to the server using the given
331374 # {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
332- # and credentials. Enters the "_authenticated_" state.
375+ # and credentials. Enters the +authenticated+ state.
333376 #
334377 # <em>The server should list <tt>"AUTH=#{mechanism}"</tt> capabilities for
335378 # supported mechanisms.</em>
336379 # - #login: Identifies the client to the server using a plain text password.
337- # Using #authenticate is generally preferred. Enters the "_authenticated_"
338- # state.
380+ # Using #authenticate is preferred. Enters the +authenticated+ state.
339381 #
340382 # <em>The +LOGINDISABLED+ capability</em> <b>must NOT</b> <em>be listed.</em>
341383 #
342384 # ==== Authenticated state
343385 #
344386 # In addition to the commands for any state, the following commands are valid
345- # in the "_authenticated_" state:
387+ # in the +authenticated+ state:
346388 #
347389 # - #enable: Enables backwards incompatible server extensions.
348390 # <em>Requires the +ENABLE+ or +IMAP4rev2+ capability.</em>
349- # - #select: Open a mailbox and enter the "_selected_" state.
350- # - #examine: Open a mailbox read-only, and enter the "_selected_" state.
391+ # - #select: Open a mailbox and enter the +selected+ state.
392+ # - #examine: Open a mailbox read-only, and enter the +selected+ state.
351393 # - #create: Creates a new mailbox.
352394 # - #delete: Permanently remove a mailbox.
353395 # - #rename: Change the name of a mailbox.
@@ -369,12 +411,12 @@ module Net
369411 #
370412 # ==== Selected state
371413 #
372- # In addition to the commands for any state and the "_authenticated_"
373- # commands, the following commands are valid in the "_selected_" state:
414+ # In addition to the commands for any state and the +authenticated+
415+ # commands, the following commands are valid in the +selected+ state:
374416 #
375- # - #close: Closes the mailbox and returns to the "_authenticated_" state,
417+ # - #close: Closes the mailbox and returns to the +authenticated+ state,
376418 # expunging deleted messages, unless the mailbox was opened as read-only.
377- # - #unselect: Closes the mailbox and returns to the "_authenticated_" state,
419+ # - #unselect: Closes the mailbox and returns to the +authenticated+ state,
378420 # without expunging any messages.
379421 # <em>Requires the +UNSELECT+ or +IMAP4rev2+ capability.</em>
380422 # - #expunge: Permanently removes messages which have the Deleted flag set.
@@ -395,7 +437,7 @@ module Net
395437 #
396438 # ==== Logout state
397439 #
398- # No \IMAP commands are valid in the "_logout_" state. If the socket is still
440+ # No \IMAP commands are valid in the +logout+ state. If the socket is still
399441 # open, Net::IMAP will close it after receiving server confirmation.
400442 # Exceptions will be raised by \IMAP commands that have already started and
401443 # are waiting for a response, as well as any that are called after logout.
@@ -449,7 +491,7 @@ module Net
449491 # ==== RFC3691: +UNSELECT+
450492 # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051] and also included
451493 # above with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands].
452- # - #unselect: Closes the mailbox and returns to the "_authenticated_" state,
494+ # - #unselect: Closes the mailbox and returns to the +authenticated+ state,
453495 # without expunging any messages.
454496 #
455497 # ==== RFC4314: +ACL+
@@ -719,14 +761,15 @@ module Net
719761 # * {IMAP URLAUTH Authorization Mechanism Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml]
720762 #
721763 class IMAP < Protocol
722- VERSION = "0.4.19 "
764+ VERSION = "0.4.21 "
723765
724766 # Aliases for supported capabilities, to be used with the #enable command.
725767 ENABLE_ALIASES = {
726768 utf8 : "UTF8=ACCEPT" ,
727769 "UTF8=ONLY" => "UTF8=ACCEPT" ,
728770 } . freeze
729771
772+ autoload :ResponseReader , File . expand_path ( "imap/response_reader" , __dir__ )
730773 autoload :SASL , File . expand_path ( "imap/sasl" , __dir__ )
731774 autoload :SASLAdapter , File . expand_path ( "imap/sasl_adapter" , __dir__ )
732775 autoload :StringPrep , File . expand_path ( "imap/stringprep" , __dir__ )
@@ -741,9 +784,11 @@ class IMAP < Protocol
741784 def self . config ; Config . global end
742785
743786 # Returns the global debug mode.
787+ # Delegates to {Net::IMAP.config.debug}[rdoc-ref:Config#debug].
744788 def self . debug ; config . debug end
745789
746790 # Sets the global debug mode.
791+ # Delegates to {Net::IMAP.config.debug=}[rdoc-ref:Config#debug=].
747792 def self . debug = ( val )
748793 config . debug = val
749794 end
@@ -764,7 +809,7 @@ class << self
764809 alias default_ssl_port default_tls_port
765810 end
766811
767- # Returns the initial greeting the server, an UntaggedResponse.
812+ # Returns the initial greeting sent by the server, an UntaggedResponse.
768813 attr_reader :greeting
769814
770815 # The client configuration. See Net::IMAP::Config.
@@ -773,13 +818,28 @@ class << self
773818 # Net::IMAP.config.
774819 attr_reader :config
775820
776- # Seconds to wait until a connection is opened.
777- # If the IMAP object cannot open a connection within this time,
778- # it raises a Net::OpenTimeout exception. The default value is 30 seconds .
779- def open_timeout ; config . open_timeout end
821+ ##
822+ # :attr_reader: open_timeout
823+ # Seconds to wait until a connection is opened. Also used by #starttls .
824+ # Delegates to { config.open_timeout}[rdoc-ref:Config#open_timeout].
780825
826+ ##
827+ # :attr_reader: idle_response_timeout
781828 # Seconds to wait until an IDLE response is received.
782- def idle_response_timeout ; config . idle_response_timeout end
829+ # Delegates to {config.idle_response_timeout}[rdoc-ref:Config#idle_response_timeout].
830+
831+ ##
832+ # :attr_accessor: max_response_size
833+ #
834+ # The maximum allowed server response size, in bytes.
835+ # Delegates to {config.max_response_size}[rdoc-ref:Config#max_response_size].
836+
837+ # :stopdoc:
838+ def open_timeout ; config . open_timeout end
839+ def idle_response_timeout ; config . idle_response_timeout end
840+ def max_response_size ; config . max_response_size end
841+ def max_response_size = ( val ) config . max_response_size = val end
842+ # :startdoc:
783843
784844 # The hostname this client connected to
785845 attr_reader :host
@@ -835,6 +895,12 @@ def idle_response_timeout; config.idle_response_timeout end
835895 #
836896 # See DeprecatedClientOptions.new for deprecated SSL arguments.
837897 #
898+ # [response_handlers]
899+ # A list of response handlers to be added before the receiver thread is
900+ # started. This ensures every server response is handled, including the
901+ # #greeting. Note that the greeting is handled in the current thread, but
902+ # all other responses are handled in the receiver thread.
903+ #
838904 # [config]
839905 # A Net::IMAP::Config object to use as the basis for #config. By default,
840906 # the global Net::IMAP.config is used.
@@ -906,7 +972,7 @@ def idle_response_timeout; config.idle_response_timeout end
906972 # [Net::IMAP::ByeResponseError]
907973 # Connected to the host successfully, but it immediately said goodbye.
908974 #
909- def initialize ( host , port : nil , ssl : nil ,
975+ def initialize ( host , port : nil , ssl : nil , response_handlers : nil ,
910976 config : Config . global , **config_options )
911977 super ( )
912978 # Config options
@@ -929,6 +995,7 @@ def initialize(host, port: nil, ssl: nil,
929995 @receiver_thread = nil
930996 @receiver_thread_exception = nil
931997 @receiver_thread_terminating = false
998+ response_handlers &.each do add_response_handler ( _1 ) end
932999
9331000 # Client Protocol Sender (including state for currently running commands)
9341001 @tag_prefix = "RUBY"
@@ -944,6 +1011,7 @@ def initialize(host, port: nil, ssl: nil,
9441011 # Connection
9451012 @tls_verified = false
9461013 @sock = tcp_socket ( @host , @port )
1014+ @reader = ResponseReader . new ( self , @sock )
9471015 start_tls_session if ssl_ctx
9481016 start_imap_connection
9491017
@@ -1204,6 +1272,10 @@ def logout!
12041272 # both successful. Any error indicates that the connection has not been
12051273 # secured.
12061274 #
1275+ # After the server agrees to start a TLS connection, this method waits up to
1276+ # {config.open_timeout}[rdoc-ref:Config#open_timeout] before raising
1277+ # +Net::OpenTimeout+.
1278+ #
12071279 # *Note:*
12081280 # >>>
12091281 # Any #response_handlers added before STARTTLS should be aware that the
@@ -2706,6 +2778,10 @@ def response_handlers
27062778 # end
27072779 # }
27082780 #
2781+ # Response handlers can also be added when the client is created before the
2782+ # receiver thread is started, by the +response_handlers+ argument to ::new.
2783+ # This ensures every server response is handled, including the #greeting.
2784+ #
27092785 # Related: #remove_response_handler, #response_handlers
27102786 def add_response_handler ( handler = nil , &block )
27112787 raise ArgumentError , "two Procs are passed" if handler && block
@@ -2732,6 +2808,7 @@ def remove_response_handler(handler)
27322808 def start_imap_connection
27332809 @greeting = get_server_greeting
27342810 @capabilities = capabilities_from_resp_code @greeting
2811+ @response_handlers . each do |handler | handler . call ( @greeting ) end
27352812 @receiver_thread = start_receiver_thread
27362813 rescue Exception
27372814 @sock . close
@@ -2860,23 +2937,10 @@ def get_tagged_response(tag, cmd, timeout = nil)
28602937 end
28612938
28622939 def get_response
2863- buff = String . new
2864- while true
2865- s = @sock . gets ( CRLF )
2866- break unless s
2867- buff . concat ( s )
2868- if /\{ (\d +)\} \r \n /n =~ s
2869- s = @sock . read ( $1. to_i )
2870- buff . concat ( s )
2871- else
2872- break
2873- end
2874- end
2940+ buff = @reader . read_response_buffer
28752941 return nil if buff . length == 0
2876- if config . debug?
2877- $stderr. print ( buff . gsub ( /^/n , "S: " ) )
2878- end
2879- return @parser . parse ( buff )
2942+ $stderr. print ( buff . gsub ( /^/n , "S: " ) ) if config . debug?
2943+ @parser . parse ( buff )
28802944 end
28812945
28822946 #############################
@@ -3077,6 +3141,7 @@ def start_tls_session
30773141 raise "already using SSL" if @sock . kind_of? ( OpenSSL ::SSL ::SSLSocket )
30783142 raise "cannot start TLS without SSLContext" unless ssl_ctx
30793143 @sock = SSLSocket . new ( @sock , ssl_ctx )
3144+ @reader = ResponseReader . new ( self , @sock )
30803145 @sock . sync_close = true
30813146 @sock . hostname = @host if @sock . respond_to? :hostname=
30823147 ssl_socket_connect ( @sock , open_timeout )
0 commit comments