Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package hawk2 for openSUSE:Factory checked 
in at 2026-05-18 17:48:30
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/hawk2 (Old)
 and      /work/SRC/openSUSE:Factory/.hawk2.new.1966 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "hawk2"

Mon May 18 17:48:30 2026 rev:39 rq:1353792 version:2.7.0+git.1772201206.4725acc7

Changes:
--------
--- /work/SRC/openSUSE:Factory/hawk2/hawk2.changes      2026-04-09 
17:20:30.261015181 +0200
+++ /work/SRC/openSUSE:Factory/.hawk2.new.1966/hawk2.changes    2026-05-18 
17:49:25.331787165 +0200
@@ -1,0 +2,6 @@
+Mon May 18 10:57:43 UTC 2026 - Aleksei Burlakov <[email protected]>
+
+- bump net-imap 0.6.3 --> 0.6.4 (bsc#1265364,bsc#1265368)
+  * net-imap-0.6.4.gem
+
+-------------------------------------------------------------------

Old:
----
  net-imap-0.6.3.gem

New:
----
  net-imap-0.6.4.gem

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ hawk2.spec ++++++
--- /var/tmp/diff_new_pack.hXdyKm/_old  2026-05-18 17:49:28.355912128 +0200
+++ /var/tmp/diff_new_pack.hXdyKm/_new  2026-05-18 17:49:28.355912128 +0200
@@ -96,7 +96,7 @@
 Source48:       mini_mime-1.1.5.gem
 Source49:       mini_portile2-2.8.9.gem
 Source50:       minitest-6.0.3.gem
-Source51:       net-imap-0.6.3.gem
+Source51:       net-imap-0.6.4.gem
 Source52:       net-pop-0.1.2.gem
 Source53:       net-protocol-0.2.2.gem
 Source54:       net-smtp-0.5.1.gem

++++++ gemfile-lock.patch ++++++
--- /var/tmp/diff_new_pack.hXdyKm/_old  2026-05-18 17:49:28.687925848 +0200
+++ /var/tmp/diff_new_pack.hXdyKm/_new  2026-05-18 17:49:28.691926013 +0200
@@ -178,7 +178,7 @@
 +    minitest (6.0.3)
 +      drb (~> 2.0)
 +      prism (~> 1.5)
-+    net-imap (0.6.3)
++    net-imap (0.6.4)
 +      date
 +      net-protocol
 +    net-pop (0.1.2)
@@ -364,7 +364,7 @@
 +  mini_mime (1.1.5) 
sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
 +  mini_portile2 (2.8.9) 
sha256=0cd7c7f824e010c072e33f68bc02d85a00aeb6fce05bb4819c03dfd3c140c289
 +  minitest (6.0.3) 
sha256=88ac8a1de36c00692420e7cb3cc11a0773bbcb126aee1c249f320160a7d11411
-+  net-imap (0.6.3) 
sha256=9bab75f876596d09ee7bf911a291da478e0cd6badc54dfb82874855ccc82f2ad
++  net-imap (0.6.4) 
sha256=9a5598c67a3022c284d98430ef1d4948e7dbdb62596f61081ea8ca933270a02b
 +  net-pop (0.1.2) 
sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3
 +  net-protocol (0.2.2) 
sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8
 +  net-smtp (0.5.1) 
sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736

++++++ net-imap-0.6.3.gem -> net-imap-0.6.4.gem ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/.document new/.document
--- old/.document       1970-01-01 01:00:00.000000000 +0100
+++ new/.document       1980-01-02 01:00:00.000000000 +0100
@@ -0,0 +1,3 @@
+*.rdoc
+*.md
+lib
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/.rdoc_options new/.rdoc_options
--- old/.rdoc_options   1970-01-01 01:00:00.000000000 +0100
+++ new/.rdoc_options   1980-01-02 01:00:00.000000000 +0100
@@ -0,0 +1,7 @@
+---
+charset: UTF-8
+main_page: README.md
+markup: rdoc
+title: net-imap # rake task override's title to add the version number
+op_dir: doc
+# vim:ft=yaml
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Gemfile new/Gemfile
--- old/Gemfile 1980-01-02 01:00:00.000000000 +0100
+++ new/Gemfile 1980-01-02 01:00:00.000000000 +0100
@@ -11,7 +11,7 @@
 
 gem "irb"
 gem "rake"
-gem "rdoc"
+gem "rdoc", ">= 7.2.0"
 gem "test-unit"
 gem "test-unit-ruby-core", git: "https://github.com/ruby/test-unit-ruby-core";
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/README.md new/README.md
--- old/README.md       1980-01-02 01:00:00.000000000 +0100
+++ new/README.md       1980-01-02 01:00:00.000000000 +0100
@@ -61,9 +61,9 @@
   else
     imap.copy(message_id, "Mail/sent-apr03")
     imap.store(message_id, "+FLAGS", [:Deleted])
+    imap.expunge
   end
 end
-imap.expunge
 ```
 
 ## Development
Binary files old/checksums.yaml.gz and new/checksums.yaml.gz differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/net/imap/command_data.rb 
new/lib/net/imap/command_data.rb
--- old/lib/net/imap/command_data.rb    1980-01-02 01:00:00.000000000 +0100
+++ new/lib/net/imap/command_data.rb    1980-01-02 01:00:00.000000000 +0100
@@ -4,6 +4,8 @@
 
 require_relative "errors"
 
+# :enddoc:
+
 module Net
   class IMAP < Protocol
 
@@ -25,6 +27,7 @@
         end
       when Time, Date, DateTime
       when Symbol
+        Flag.validate(data)
       else
         data.validate
       end
@@ -45,7 +48,7 @@
       when Date
         send_date_data(data)
       when Symbol
-        send_symbol_data(data)
+        Flag[data].send_data(self, tag)
       else
         data.send_data(self, tag)
       end
@@ -77,9 +80,23 @@
       put_string('"' + str.gsub(/["\\]/, "\\\\\\&") + '"')
     end
 
-    def send_literal(str, tag = nil)
+    def send_binary_literal(*, **) = send_literal(*, **, binary: true)
+
+    # `non_sync` is an optional tri-state flag:
+    # * `true`  -> Force non-synchronizing `LITERAL+`/`LITERAL-` behavior.
+    #   TODO: raise or warn when capabilities don't allow non_sync.
+    # * `false` -> Force normal synchronizing literal behavior.
+    # * `nil`   -> (default) Currently behaves like `false` (will be dynamic).
+    def send_literal(str, tag = nil, binary: false, non_sync: nil)
       synchronize do
-        put_string("{" + str.bytesize.to_s + "}" + CRLF)
+        non_sync = non_sync_literal?(str.bytesize) if non_sync.nil?
+        prefix = "~" if binary
+        plus = "+" if non_sync
+        put_string("#{prefix}{#{str.bytesize}#{plus}}\r\n")
+        if non_sync
+          put_string(str)
+          return
+        end
         @continued_command_tag = tag
         @continuation_request_exception = nil
         begin
@@ -94,6 +111,13 @@
       end
     end
 
+    def non_sync_literal?(bytesize)
+      capabilities_cached? &&
+        bytesize <= config.max_non_synchronizing_literal &&
+        (capable?("LITERAL+") ||
+         bytesize <= 4096 && (capable?("IMAP4rev2") || capable?("LITERAL-")))
+    end
+
     def send_number_data(num)
       put_string(num.to_s)
     end
@@ -115,11 +139,13 @@
     def send_date_data(date) put_string Net::IMAP.encode_date(date) end
     def send_time_data(time) put_string Net::IMAP.encode_time(time) end
 
-    def send_symbol_data(symbol)
-      put_string("\\" + symbol.to_s)
-    end
-
     CommandData = Data.define(:data) do # :nodoc:
+      def self.validate(...)
+        data = new(...)
+        data.validate
+        data
+      end
+
       def send_data(imap, tag)
         raise NoMethodError, "#{self.class} must implement #{__method__}"
       end
@@ -128,15 +154,109 @@
       end
     end
 
+    # Represents IMAP +text+ data, which may contain any 7-bit ASCII character,
+    # except for +NULL+, +CR+, or +LF+.  +text+ is extended to allow any
+    # multibyte +UTF-8+ character when either +UTF8=ACCEPT+ or +IMAP4rev2+ have
+    # been enabled, or when the server supports only +IMAP4rev2+ and not 
earlier
+    # IMAP revisions, or when the server advertises +UTF8=ONLY+.
+    #
+    # NOTE: The current implementation does not validate whether the connection
+    # currently supports UTF-8.  Future versions may change.
+    #
+    # The string's bytes must be valid ASCII or valid UTF-8.  The string's
+    # reported encoding is ignored, but the string is _not_ transcoded.
+    class RawText < CommandData # :nodoc:
+      def initialize(data:)
+        data = String(data.to_str)
+        data = if data.encoding in Encoding::ASCII | Encoding::UTF_8
+          -data
+        elsif data.ascii_only?
+          -(data.dup.force_encoding("ASCII"))
+        else
+          -(data.dup.force_encoding("UTF-8"))
+        end
+        super
+        validate
+      end
+
+      def validate
+        if data.include?("\0")
+          raise DataFormatError, "NULL byte must be binary literal encoded"
+        elsif !data.valid_encoding?
+          raise DataFormatError, "invalid UTF-8 must be literal encoded"
+        elsif /[\r\n]/.match?(data)
+          raise DataFormatError, "CR and LF bytes must be literal encoded"
+        end
+      end
+
+      def ascii_only? = data.ascii_only?
+
+      def send_data(imap, tag) = imap.__send__(:put_string, data)
+    end
+
     class RawData < CommandData # :nodoc:
-      def send_data(imap, tag)
-        imap.__send__(:put_string, data)
+      def initialize(data:)
+        data = split_parts(data)
+        super
+        validate
+      end
+
+      def send_data(imap, tag) = data.each do _1.send_data(imap, tag) end
+
+      def validate
+        return unless data.last in RawText(data: text)
+        if text.rindex(/~?\{[1-9]\d*\+?\}\z/n)
+          raise DataFormatError, "RawData cannot end with literal continuation"
+        end
+      end
+
+      private
+
+      def split_parts(data)
+        data = data.b # dups and ensures BINARY encoding
+        parts = []
+        while data.match(/(~)?\{(0|[1-9]\d*)(\+)?\}\r\n/n)
+          text, binary, bytesize, non_sync, data = $`, !!$1, $2, !!$3, $'
+          bytesize = NumValidator.coerce_number64 bytesize
+          parts << RawText[text] unless text.empty?
+          parts << extract_literal(data, binary:, bytesize:, non_sync:)
+          data.bytesplice(0, bytesize, "")
+        end
+        parts << RawText[data] unless data.empty?
+        parts
+      end
+
+      def extract_literal(data, binary:, bytesize:, non_sync:)
+        if data.bytesize < bytesize
+          raise DataFormatError, "Too few bytes in string for literal, " \
+            "expected: %s, remaining: %s" % [bytesize, data.bytesize]
+        end
+        literal = data.byteslice(0, bytesize)
+        (binary ? Literal8 : Literal).new(data: literal, non_sync:)
       end
     end
 
     class Atom < CommandData # :nodoc:
+      def initialize(**)
+        super
+        validate
+      end
+
+      def validate
+        data.to_s.ascii_only? \
+          or raise DataFormatError, "#{self.class} must be ASCII only"
+        data.match?(ResponseParser::Patterns::ATOM_SPECIALS) \
+          and raise DataFormatError, "#{self.class} must not contain 
atom-specials"
+      end
+
+      def send_data(imap, tag)
+        imap.__send__(:put_string, data.to_s)
+      end
+    end
+
+    class Flag < Atom # :nodoc:
       def send_data(imap, tag)
-        imap.__send__(:put_string, data)
+        imap.__send__(:put_string, "\\#{data}")
       end
     end
 
@@ -146,9 +266,39 @@
       end
     end
 
-    class Literal < CommandData # :nodoc:
+    class Literal < Data.define(:data, :non_sync) # :nodoc:
+      def self.validate(...)
+        data = new(...)
+        data.validate
+        data
+      end
+
+      def initialize(data:, non_sync: nil)
+        data = -String(data.to_str).b or
+          raise DataFormatError, "#{self.class} expects string input"
+        super
+        validate
+      end
+
+      def bytesize = data.bytesize
+
+      def validate
+        if data.include?("\0")
+          raise DataFormatError, "NULL byte not allowed in #{self.class}.  " \
+            "Use #{Literal8} or a null-safe encoding."
+        end
+      end
+
+      def send_data(imap, tag)
+        imap.__send__(:send_literal, data, tag, non_sync:)
+      end
+    end
+
+    class Literal8 < Literal # :nodoc:
+      def validate = nil # all bytes are okay
+
       def send_data(imap, tag)
-        imap.__send__(:send_literal, data, tag)
+        imap.__send__(:send_binary_literal, data, tag, non_sync:)
       end
     end
 
@@ -221,6 +371,14 @@
 
       module_function
 
+      def literal_or_literal8(input, name: "argument")
+        return input if input in Literal | Literal8
+        data = String.try_convert(input) \
+          or raise TypeError, "expected #{name} to be String, got 
#{input.class}"
+        type = data.include?("\0") ? Literal8 : Literal
+        type.new(data:)
+      end
+
       # Allows symbols in addition to strings
       def valid_string?(str)
         str.is_a?(Symbol) || str.respond_to?(:to_str)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/net/imap/config.rb new/lib/net/imap/config.rb
--- old/lib/net/imap/config.rb  1980-01-02 01:00:00.000000000 +0100
+++ new/lib/net/imap/config.rb  1980-01-02 01:00:00.000000000 +0100
@@ -281,6 +281,40 @@
         0.5r => true,
       }
 
+      # The maximum bytesize for sending non-synchronizing literals, when the
+      # server supports them.  To disable non-synchronizing literals, set the
+      # value to +-1+.
+      #
+      # Non-synchronizing literals are only sent when the server's
+      # capabilities[rdoc-ref:IMAP#capabilities] have been
+      # cached[rdoc-ref:IMAP#capabilities_cached?] and include either
+      # <tt>LITERAL+</tt> [RFC7888[https://www.rfc-editor.org/rfc/rfc7888]],
+      # <tt>LITERAL-</tt> [RFC7888[https://www.rfc-editor.org/rfc/rfc7888]], or
+      # +IMAP4rev2+ [RFC9051[https://www.rfc-editor.org/rfc/rfc9051]].
+      #
+      # For <tt>LITERAL+</tt>, this value is the only limit on whether a 
literal
+      # value is sent as non-synchronizing literals.  For <tt>LITERAL-</tt> and
+      # <tt>IMAP4rev2</tt>, non-synchronizing literals must also be smaller 
than
+      # +4096+ bytes.
+      #
+      # Non-synchronizing literals avoid the latency of waiting for the server
+      # to allow continuation.  However, if a client sends a non-synchronizing
+      # literal that is too large for the server, the server may need to close
+      # the connection.  Because <tt>LITERAL+</tt> does not directly indicate
+      # the server's limits, it's best to avoid sending very large
+      # non-synchronized literals.
+      #
+      # ==== Versioned Defaults
+      #
+      # max_non_synchronizing_literal <em>was added in +v0.6.4+.</em>
+      #
+      # * original: +-1+ (_never_ send non-synchronizing literals)
+      # * +0.6+: 16 KiB
+      attr_accessor :max_non_synchronizing_literal, type: Integer?, defaults: {
+        0.0r => -1,
+        0.6r => 16 << 16, # 16 KiB
+      }
+
       # The maximum allowed server response size.  When +nil+, there is no 
limit
       # on response size.
       #
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/net/imap/data_encoding.rb 
new/lib/net/imap/data_encoding.rb
--- old/lib/net/imap/data_encoding.rb   1980-01-02 01:00:00.000000000 +0100
+++ new/lib/net/imap/data_encoding.rb   1980-01-02 01:00:00.000000000 +0100
@@ -174,6 +174,22 @@
         0 < num && num <= 0xffff_ffff
       end
 
+      # Check if argument is a valid 'number64' according to RFC 9051
+      #     number64        = 1*DIGIT
+      #                        ; Unsigned 63-bit integer
+      #                        ; (0 <= n <= 9,223,372,036,854,775,807)
+      def valid_number64?(num)
+        0 <= num && num <= 0x7fff_ffff_ffff_ffff
+      end
+
+      # Check if argument is a valid 'number64' according to RFC 9051
+      #     nz-number64     = digit-nz *DIGIT
+      #                        ; Unsigned 63-bit integer
+      #                        ; (0 < n <= 9,223,372,036,854,775,807)
+      def valid_nz_number64?(num)
+        0 < num && num <= 0x7fff_ffff_ffff_ffff
+      end
+
       # Check if argument is a valid 'mod-sequence-value' according to RFC 4551
       #     mod-sequence-value  = 1*DIGIT
       #                            ; Positive unsigned 64-bit integer
@@ -203,6 +219,20 @@
           "nz-number must be non-zero unsigned 32-bit integer: #{num}"
       end
 
+      # Ensure argument is 'number64' or raise DataFormatError
+      def ensure_number64(num)
+        return num if valid_number64?(num)
+        raise DataFormatError,
+          "number64 must be unsigned 63-bit integer: #{num}"
+      end
+
+      # Ensure argument is 'nz-number64' or raise DataFormatError
+      def ensure_nz_number64(num)
+        return num if valid_nz_number64?(num)
+        raise DataFormatError,
+          "nz-number64 must be non-zero unsigned 63-bit integer: #{num}"
+      end
+
       # Ensure argument is 'mod-sequence-value' or raise DataFormatError
       def ensure_mod_sequence_value(num)
         return num if valid_mod_sequence_value?(num)
@@ -237,6 +267,26 @@
         end
       end
 
+      # Like #ensure_number64, but usable with numeric String input.
+      def coerce_number64(num)
+        case num
+        when Integer   then ensure_number64 num
+        when NUMBER_RE then ensure_number64 Integer num
+        else
+          raise DataFormatError, "%p is not a valid number64" % [num]
+        end
+      end
+
+      # Like #ensure_nz_number64, but usable with numeric String input.
+      def coerce_nz_number64(num)
+        case num
+        when Integer   then ensure_nz_number64 num
+        when NUMBER_RE then ensure_nz_number64 Integer num
+        else
+          raise DataFormatError, "%p is not a valid nz-number64" % [num]
+        end
+      end
+
       # Like #ensure_mod_sequence_value, but usable with numeric String input.
       def coerce_mod_sequence_value(num)
         case num
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/net/imap/errors.rb new/lib/net/imap/errors.rb
--- old/lib/net/imap/errors.rb  1980-01-02 01:00:00.000000000 +0100
+++ new/lib/net/imap/errors.rb  1980-01-02 01:00:00.000000000 +0100
@@ -172,18 +172,11 @@
           ]
         end
         if parser_backtrace
-          backtrace_locations&.each_with_index do |loc, idx|
-            next  if    loc.base_label.include? "parse_error"
-            break if    loc.base_label == "parse"
-            if loc.label.include?("#") # => Class#method, since ruby 3.4
-              next unless loc.label&.include?(parser_class.name)
-            else
-              next unless loc.path&.include?("net/imap/response_parser")
-            end
+          normalized_parser_backtrace.each do |idx, path, lineno, label, 
base_label|
             msg << "\n  %s: %s (%s:%d)" % [
               hl["%{key}caller[%{/key}%{idx}%%2d%{/idx}%{key}]%{/key}"] % idx,
-              hl["%{label}%%-30s%{/label}"] % loc.base_label,
-              File.basename(loc.path, ".rb"), loc.lineno
+              hl["%{label}%%-30s%{/label}"] % base_label,
+              File.basename(path, ".rb"), lineno
             ]
           end
         end
@@ -198,12 +191,56 @@
       def processed_string = string && pos && string[...pos]
       def remaining_string = string && pos && string[pos..]
 
+      # Returns true when all attributes are equal, except for #backtrace and
+      # #backtrace_locations which are replaced with #parser_methods.  This
+      # allows deserialized errors to be compared.
+      def ==(other)
+        return false if self.class != other.class
+        methods       = parser_methods
+        other_methods = other.parser_methods
+        message     == other.message &&
+          methods   == other_methods &&
+          string    == other.string &&
+          pos       == other.pos &&
+          lex_state == other.lex_state &&
+          token     == other.token
+      end
+
+      # Lists the methods (from #backtrace_locations or #backtrace) called on
+      # parser_class (since ruby 3.4) or which have "net/imap/response_parser"
+      # in the path (before ruby 3.4).  Most parser method names are based on
+      # rules in the IMAP grammar.
+      def parser_methods = normalized_parser_backtrace.map(&:last)
+
       private
 
+      def normalized_parser_backtrace
+        normalize_backtrace
+          .take_while {|_, _, _, _, base_label| base_label != "parse" }
+          .reject {|_, _, _, _, base_label| base_label.nil? }
+          .reject {|_, _, _, _, base_label| base_label.include? "parse_error" }
+          .select {|_, path, _, label, _|
+            if label.include?("#") # => Class#method, since ruby 3.4
+              label.include?(parser_class.name)
+            else
+              path.include?("net/imap/response_parser")
+            end
+          }
+      end
+
+      def normalize_backtrace
+        (backtrace_locations&.each_with_index&.map {|loc, idx|
+          [idx, loc.path, loc.lineno, loc.label, loc.base_label]
+        } || backtrace&.each_with_index&.map {|bt, idx|
+          [idx, *bt.match(/\A(\S+):(\d+):in 
[`'](.*?([\w]+[?!]?))'\z/)&.captures]
+        } || [])
+      end
+
       def default_highlight_from_env
         (ENV["FORCE_COLOR"] || "") !~ /\A(?:0|)\z/ ||
           (ENV["TERM"] || "") !~ /\A(?:dumb|unknown|)\z/i
       end
+
     end
 
     # Superclass of all errors used to encapsulate "fail" responses
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/net/imap/response_data.rb 
new/lib/net/imap/response_data.rb
--- old/lib/net/imap/response_data.rb   1980-01-02 01:00:00.000000000 +0100
+++ new/lib/net/imap/response_data.rb   1980-01-02 01:00:00.000000000 +0100
@@ -75,9 +75,18 @@
     #
     # Net::IMAP::UnparsedData represents data for unknown response types or
     # unknown extensions to response types without a well-defined extension
-    # grammar.
+    # grammar.  UnparsedData represents the portion of the response which the
+    # parser has skipped over, without attempting to parse it.
     #
-    # See also: UnparsedNumericResponseData, ExtensionData, IgnoredResponse
+    #    parser = Net::IMAP::ResponseParser.new
+    #    response = parser.parse "* X-UNKNOWN-TYPE can't parse this\r\n"
+    #    response => Net::IMAP::UntaggedResponse(
+    #      name: "X-UNKNOWN-TYPE",
+    #      data: Net::IMAP::UnparsedData(unparsed_data: "can't parse this"),
+    #    )
+    #
+    # See also: UnparsedNumericResponseData, ExtensionData, IgnoredResponse,
+    # InvalidParseData.
     class UnparsedData < Struct.new(:unparsed_data)
       ##
       # method: unparsed_data
@@ -90,10 +99,75 @@
     # instances of this class are returned, future releases may return a
     # different (incompatible) object <em>without deprecation or warning</em>.
     #
+    # When the response parser encounters a recoverable error,
+    # Net::IMAP::InvalidParseData represents that portion of the response which
+    # could not be parsed, allowing the parser to parse the remainder of the
+    # response.  InvalidParseData is always associated with a 
ResponseParseError
+    # which has been rescued.
+    #
+    # This could be caused by a malformed server response, by a bug in
+    # Net::IMAP::ResponseParser, or by an unsupported extension to the response
+    # syntax.  For example, if a server supports +UIDPLUS+, but sends an 
invalid
+    # +COPYUID+ response code:
+    #
+    #    parser = Net::IMAP::ResponseParser.new
+    #    parsed = parser.parse "* OK [COPYUID 701  ] copied one message\r\n"
+    #    parsed => {
+    #      data: Net::IMAP::ResponseText(
+    #        code: Net::IMAP::ResponseCode(
+    #          name: "COPYUID",
+    #          data: Net::IMAP::InvalidParseData(
+    #            parse_error: Net::IMAP::ResponseParseError,
+    #            unparsed_data: "701  ",
+    #            parsed_data: nil,
+    #          )
+    #        )
+    #      )
+    #    }
+    #
+    # In this example, although <tt>[COPYUID 701  ]</tt> uses valid syntax for 
a
+    # _generic_ ResponseCode, it is _invalid_ syntax for a +COPYUID+ response
+    # code.
+    #
+    # See also: UnparsedData, ExtensionData
+    class InvalidParseData < Data.define(:parse_error, :unparsed_data, 
:parsed_data)
+      ##
+      # method: parse_error
+      # :call-seq: parse_error -> ResponseParseError
+      #
+      # Returns the rescued ResponseParseError.
+
+      ##
+      # method: unparsed_data
+      # :call-seq: unparsed_data -> string
+      #
+      # Returns the raw string which was skipped over by the parser.
+
+      ##
+      # method: parsed_data
+      #
+      # May return a partial parse result for unparsed_data, which had already
+      # been parsed before the parse_error.
+    end
+
+    # **Note:** This represents an intentionally _unstable_ API.  Where
+    # instances of this class are returned, future releases may return a
+    # different (incompatible) object <em>without deprecation or warning</em>.
+    #
     # Net::IMAP::UnparsedNumericResponseData represents data for unhandled
     # response types with a numeric prefix.  See the documentation for #number.
     #
-    # See also: UnparsedData, ExtensionData, IgnoredResponse
+    #    parser = Net::IMAP::ResponseParser.new
+    #    response = parser.parse "* 123 X-UNKNOWN-TYPE can't parse this\r\n"
+    #    response => Net::IMAP::UntaggedResponse(
+    #      name: "X-UNKNOWN-TYPE",
+    #      data: Net::IMAP::UnparsedNumericData(
+    #        number: 123,
+    #        unparsed_data: "can't parse this"
+    #      ),
+    #    )
+    #
+    # See also: UnparsedData, ExtensionData, IgnoredResponse, InvalidParseData
     class UnparsedNumericResponseData < Struct.new(:number, :unparsed_data)
       ##
       # method: number
@@ -306,6 +380,14 @@
     #   because the server doesn't allow deletion of mailboxes with children.
     #   #data is +nil+.
     #
+    # === <tt>QUOTA=RES-*</tt> response codes
+    # See {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html#section-4.3].
+    # * +OVERQUOTA+ (also in RFC5530[https://www.rfc-editor.org/rfc/rfc5530]),
+    #   with a tagged +NO+ response to an +APPEND+/+COPY+/+MOVE+ command when
+    #   the command would put the target mailbox over any quota, and with an
+    #   untagged +NO+ when a mailbox exceeds a soft quota (which may be caused
+    #   be external events).  #data is +nil+.
+    #
     # === +CONDSTORE+ extension
     # See {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
     # * +NOMODSEQ+, when selecting a mailbox that does not support
@@ -324,9 +406,10 @@
     #
     # Response codes are backwards compatible:  Servers are allowed to send new
     # response codes even if the client has not enabled the extension that
-    # defines them.  When Net::IMAP does not know how to parse response
-    # code text, #data returns the unparsed string.
-    #
+    # defines them.  When ResponseParser does not know how to parse the 
response
+    # code data, #data may return the unparsed string, ExtensionData, or
+    # UnparsedData.  When ResponseParser attempts but fails to parse the
+    # response code data, #data returns InvalidParseData.
     class ResponseCode < Struct.new(:name, :data)
       ##
       # method: name
@@ -341,8 +424,13 @@
       #
       # Returns the parsed response code data, e.g: an array of capabilities
       # strings, an array of character set strings, a list of permanent flags,
-      # an Integer, etc.  The response #code determines what form the response
-      # code data can take.
+      # an Integer, etc.  The response #name determines what form the response
+      # code #data can take.
+      #
+      # When ResponseParser does not know how to parse the response code data,
+      # #data may return the unparsed string, ExtensionData, or UnparsedData.
+      # When ResponseParser attempts but fails to parse the response code data,
+      # #data returns InvalidParseData.
     end
 
     # MailboxList represents the data of an untagged +LIST+ response, for a
@@ -383,14 +471,23 @@
     # and MailboxQuota objects.
     #
     # == Required capability
+    #
     # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
-    # capability.
+    # or <tt>QUOTA=RES-STORAGE</tt>
+    # [RFC9208[https://www.rfc-editor.org/rfc/rfc9208]] capability.
     class MailboxQuota < Struct.new(:mailbox, :usage, :quota)
       ##
       # method: mailbox
       # :call-seq: mailbox -> string
       #
-      # The mailbox with the associated quota.
+      # The quota root with the associated quota.
+      #
+      # NOTE: this was mistakenly named "mailbox".  But the quota root's name 
may
+      # differ from the mailbox.  A single quota root may cover multiple
+      # mailboxes, and a single mailbox may be governed by multiple quota 
roots.
+
+      # The quota root with the associated quota.
+      alias quota_root mailbox
 
       ##
       # method: usage
@@ -402,7 +499,7 @@
       # method: quota
       # :call-seq: quota -> Integer
       #
-      # Quota limit imposed on the mailbox.
+      # Storage limit imposed on the mailbox.
       #
     end
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/net/imap/response_parser.rb 
new/lib/net/imap/response_parser.rb
--- old/lib/net/imap/response_parser.rb 1980-01-02 01:00:00.000000000 +0100
+++ new/lib/net/imap/response_parser.rb 1980-01-02 01:00:00.000000000 +0100
@@ -691,6 +691,8 @@
         CRLF!
         EOF!
         resp
+      rescue SystemStackError
+        parse_error("response recursion too deep")
       end
 
       # RFC3501 & RFC9051:
@@ -1961,6 +1963,7 @@
       #   resp-text-code   =/ "UIDREQUIRED"
       def resp_text_code
         name = resp_text_code__name
+        state = current_state
         data =
           case name
           when "CAPABILITY"         then resp_code__capability
@@ -1983,8 +1986,18 @@
           when "MAILBOXID"          then SP!; parens__objectid     # RFC8474: 
OBJECTID
           when "UIDREQUIRED"        then                           # RFC9586: 
UIDONLY
           else
+            state = nil # don't backtrack
             SP? and text_chars_except_rbra
           end
+        peek_rbra? or
+          parse_error("expected resp-text-code %p to be complete", name)
+        ResponseCode.new(name, data)
+      rescue Net::IMAP::ResponseParseError => parse_error
+        raise unless state
+        raise if parse_error.message.include?("uid-set")
+        restore_state state
+        unparsed_data = SP? && text_chars_except_rbra
+        data = InvalidParseData[parse_error:, unparsed_data:, parsed_data: 
data]
         ResponseCode.new(name, data)
       end
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/net/imap/response_reader.rb 
new/lib/net/imap/response_reader.rb
--- old/lib/net/imap/response_reader.rb 1980-01-02 01:00:00.000000000 +0100
+++ new/lib/net/imap/response_reader.rb 1980-01-02 01:00:00.000000000 +0100
@@ -8,51 +8,60 @@
 
       def initialize(client, sock)
         @client, @sock = client, sock
+        # cached config
+        @max_response_size = nil
+        # response buffer state
+        @buff = @literal_size = nil
       end
 
       def read_response_buffer
+        @max_response_size = client.max_response_size
         @buff = String.new
         catch :eof do
           while true
+            guard_response_too_large!
             read_line
-            break unless (@literal_size = get_literal_size)
+            # check before allocating memory for literal
+            guard_response_too_large!
+            break unless literal_size
             read_literal
           end
         end
         buff
       ensure
-        @buff = nil
+        @buff = @literal_size = nil
       end
 
       private
 
+      # cached config
+      attr_reader :max_response_size
+
+      # response buffer state
       attr_reader :buff, :literal_size
 
       def bytes_read          = buff.bytesize
       def empty?              = buff.empty?
-      def done?               = line_done? && !get_literal_size
+      def done?               = line_done? && !literal_size
       def line_done?          = buff.end_with?(CRLF)
-      def get_literal_size    = /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i
+
+      def get_literal_size(buff)
+        buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i
+      end
 
       def read_line
-        buff << (@sock.gets(CRLF, read_limit) or throw :eof)
-        max_response_remaining! unless line_done?
+        line = (@sock.gets(CRLF, max_response_remaining) or throw :eof)
+        @literal_size = get_literal_size(line)
+        buff << line
       end
 
       def read_literal
-        # check before allocating memory for literal
-        max_response_remaining!
         literal = String.new(capacity: literal_size)
-        buff << (@sock.read(read_limit(literal_size), literal) or throw :eof)
+        buff << (@sock.read(literal_size, literal) or throw :eof)
       ensure
         @literal_size = nil
       end
 
-      def read_limit(limit = nil)
-        [limit, max_response_remaining!].compact.min
-      end
-
-      def max_response_size      = client.max_response_size
       def max_response_remaining = max_response_size &.- bytes_read
       def response_too_large?    = max_response_size &.< min_response_size
       def min_response_size      = bytes_read + min_response_remaining
@@ -61,8 +70,8 @@
         empty? ? 3 : done? ? 0 : (literal_size || 0) + 2
       end
 
-      def max_response_remaining!
-        return max_response_remaining unless response_too_large?
+      def guard_response_too_large!
+        return unless response_too_large?
         raise ResponseTooLargeError.new(
           max_response_size:, bytes_read:, literal_size:,
         )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/net/imap/sasl/scram_authenticator.rb 
new/lib/net/imap/sasl/scram_authenticator.rb
--- old/lib/net/imap/sasl/scram_authenticator.rb        1980-01-02 
01:00:00.000000000 +0100
+++ new/lib/net/imap/sasl/scram_authenticator.rb        1980-01-02 
01:00:00.000000000 +0100
@@ -75,13 +75,19 @@
         # * #password ― Password or passphrase associated with this #username.
         # * _optional_ #authzid ― Alternate identity to act as or on behalf of.
         # * _optional_ #min_iterations - Overrides the default value (4096).
+        # * _optional_ #max_iterations - Overrides the default value (2³¹ - 1).
         #
         # Any other keyword parameters are quietly ignored.
+        #
+        # *NOTE:* <em>It is the user's responsibility</em> to enforce minimum
+        # and maximum iteration counts that are appropriate for their security
+        # context.
         def initialize(username_arg = nil, password_arg = nil,
                        authcid: nil, username: nil,
                        authzid: nil,
                        password: nil, secret: nil,
                        min_iterations: 4096, # see both RFC5802 and RFC7677
+                       max_iterations: 2**31 - 1,  # max int32
                        cnonce: nil, # must only be set in tests
                        **options)
           @username = username || username_arg || authcid or
@@ -94,7 +100,22 @@
           @min_iterations.positive? or
             raise ArgumentError, "min_iterations must be positive"
 
+          @max_iterations = Integer max_iterations.to_int
+          @min_iterations <= @max_iterations or
+            raise ArgumentError, "max_iterations must be more than 
min_iterations"
+
           @cnonce = cnonce || SecureRandom.base64(32)
+
+          # These attrs are set from the server challenges
+          @server_first_message = @snonce = @salt = @iterations = nil
+          @server_error = nil
+
+          # Memoized after @salt and @iterations have been sent.
+          @salted_password = @client_key = @server_key = nil
+
+          # These values are created and cached in response to server 
challenges
+          @client_first_message_bare = nil
+          @client_final_message_without_proof = nil
         end
 
         # Authentication identity: the identity that matches the #password.
@@ -127,8 +148,43 @@
 
         # The minimal allowed iteration count.  Lower #iterations will raise an
         # Error.
+        #
+        # *WARNING:* The default value (4096) is set to match guidance from
+        # both {RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#page-12]
+        # and RFC7677[https://www.rfc-editor.org/rfc/rfc7677#section-4], but
+        # {modern 
recommendations}[https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2]
+        # are significantly higher.
+        #
+        # It is ultimately the server's responsibility to securely store
+        # password hashes.  While this parameter can alert the user to
+        # insecure password storage and prevent insecure authentication
+        # exchange, updating the iteration count generally requires resetting
+        # the password on the server.
         attr_reader :min_iterations
 
+        # The maximal allowed iteration count.  Higher #iterations will raise 
an
+        # Error.
+        #
+        # As noted in 
{RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#section-9]
+        # >>>
+        #   A hostile server can perform a computational denial-of-service
+        #   attack on clients by sending a big iteration count value.
+        #
+        # *WARNING:* The default value is <tt>2³¹ - 1</tt>, the maximum signed
+        # 32-bit integer.  This is large enough for the computation to take
+        # several minutes, and insufficient protection against hostile servers.
+        #
+        # Note that <tt>OpenSSL::KDF.pbkdf2_hmac</tt> is implemented by a
+        # blocking C function, and cannot be interrupted by +Timeout+ or
+        # <tt>Thread.raise</tt>.  And it keeps the Global VM lock, as of v4.0 
of
+        # the +openssl+ gem, so other ruby threads will not be able to run.
+        #
+        # <em>To prevent a denial of service attack,</em> this must be set to a
+        # safe value, depending on hardware and version of OpenSSL.  <em>It is
+        # the user's responsibility</em> to enforce minimum and maximum
+        # iteration counts that are appropriate for their security context.
+        attr_reader :max_iterations
+
         # The client nonce, generated by SecureRandom
         attr_reader :cnonce
 
@@ -147,6 +203,15 @@
         # Net::IMAP::NoResponseError.
         attr_reader :server_error
 
+        # Memoized ScramAlgorithm#salted_password (needs #salt and #iterations)
+        def salted_password = @salted_password ||= compute_salted { super }
+
+        # Memoized ScramAlgorithm#client_key (needs #salt and #iterations)
+        def client_key = @client_key ||= compute_salted { super }
+
+        # Memoized ScramAlgorithm#server_key (needs #salt and #iterations)
+        def server_key = @server_key ||= compute_salted { super }
+
         # Returns a new OpenSSL::Digest object, set to the appropriate hash
         # function for the chosen mechanism.
         #
@@ -186,6 +251,13 @@
 
         private
 
+        # Checks for +salt+ and +iterations+ before yielding
+        def compute_salted
+          salt       in String  or raise Error, "unknown salt"
+          iterations in Integer or raise Error, "unknown iterations"
+          yield
+        end
+
         # Need to store this for auth_message
         attr_reader :server_first_message
 
@@ -202,6 +274,8 @@
             raise Error, "server did not send iteration count"
           min_iterations <= iterations or
             raise Error, "too few iterations: %d" % [iterations]
+          max_iterations.nil? || iterations <= max_iterations or
+            raise Error, "too many iterations: %d" % [iterations]
           mext = sparams["m"] and
             raise Error, "mandatory extension: %p" % [mext]
           snonce.start_with? cnonce or
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/net/imap/search_result.rb 
new/lib/net/imap/search_result.rb
--- old/lib/net/imap/search_result.rb   1980-01-02 01:00:00.000000000 +0100
+++ new/lib/net/imap/search_result.rb   1980-01-02 01:00:00.000000000 +0100
@@ -123,7 +123,11 @@
       #     Net::IMAP::SearchResult[9, 1, 2, 4, 10, 12, 3, modseq: 123_456]
       #       .to_sequence_set
       #     # => Net::IMAP::SequenceSet["1:4,9:10,12"]
-      def to_sequence_set; SequenceSet[*self] end
+      #
+      # >>>
+      #   *NOTE:* +SORT+ order is not preserved.  The result will be sorted.
+      #
+      def to_sequence_set; empty? ? SequenceSet.empty : SequenceSet[to_a] end
 
       def pretty_print(pp)
         return super if modseq.nil?
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/net/imap.rb new/lib/net/imap.rb
--- old/lib/net/imap.rb 1980-01-02 01:00:00.000000000 +0100
+++ new/lib/net/imap.rb 1980-01-02 01:00:00.000000000 +0100
@@ -450,8 +450,8 @@
   #
   # Although IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051] is not supported
   # yet, Net::IMAP supports several extensions that have been folded into it:
-  # +ENABLE+, +IDLE+, +MOVE+, +NAMESPACE+, +SASL-IR+, +UIDPLUS+, +UNSELECT+,
-  # <tt>STATUS=SIZE</tt>, and the fetch side of +BINARY+.
+  # +ENABLE+, +IDLE+, +LITERAL-+, +MOVE+, +NAMESPACE+, +SASL-IR+, +UIDPLUS+,
+  # +UNSELECT+, <tt>STATUS=SIZE</tt>, and the fetch side of +BINARY+.
   # Commands for these extensions are listed with the {Core IMAP
   # commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands], above.
   #
@@ -459,9 +459,12 @@
   #   <em>The following are folded into +IMAP4rev2+ but are currently
   #   unsupported or incompletely supported by</em> Net::IMAP<em>: RFC4466
   #   extensions, +SEARCHRES+, +LIST-EXTENDED+, +LIST-STATUS+,
-  #   +LITERAL-+, and +SPECIAL-USE+.</em>
+  #   and +SPECIAL-USE+.</em>
   #
   # ==== RFC2087: +QUOTA+
+  # +NOTE:+ Only the +STORAGE+ quota resource type is currently supported.
+  # - Obsoleted by <tt>QUOTA=RES-*</tt> 
[RFC9208[https://www.rfc-editor.org/rfc/rfc9208]],
+  #   although the commands are backward compatible.
   # - #getquota: returns the resource usage and limits for a quota root
   # - #getquotaroot: returns the list of quota roots for a mailbox, as well as
   #   their resource usage and limits.
@@ -486,9 +489,7 @@
   # IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051].
   # - Updates #fetch and #uid_fetch with the +BINARY+, +BINARY.PEEK+, and
   #   +BINARY.SIZE+ items.  See FetchData#binary and FetchData#binary_size.
-  #
-  # >>>
-  #   *NOTE:* The binary extension the #append command is _not_ supported yet.
+  # - Updates #append to allow binary messages containing +NULL+ bytes.
   #
   # ==== RFC3691: +UNSELECT+
   # Folded into IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051] and also 
included
@@ -568,6 +569,15 @@
   # - Updates #store and #uid_store with the +unchangedsince+ modifier and adds
   #   the +MODIFIED+ ResponseCode to the tagged response.
   #
+  # ==== RFC7888: <tt>LITERAL+</tt>
+  # - Literal strings smaller than Config#max_non_synchronizing_literal bytes
+  #   are sent without waiting for the server's continuation request.
+  #
+  # ==== RFC7888: +LITERAL-+
+  # - Literal strings smaller than 4096 bytes or
+  #   Config#max_non_synchronizing_literal (whichever is smaller)
+  #   are sent without waiting for the server's continuation request.
+  #
   # ==== RFC8438: <tt>STATUS=SIZE</tt>
   # - Updates #status with the +SIZE+ status attribute.
   #
@@ -578,6 +588,16 @@
   #   See FetchData#emailid and FetchData#emailid.
   # - Updates #status with support for the +MAILBOXID+ status attribute.
   #
+  # ==== RFC9208: <tt>QUOTA=RES-*</tt>
+  # +NOTE:+ Only the +STORAGE+ quota resource type is currently supported.
+  # - Obsoletes the +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
+  #   extension and provides strict semantics for different resource types.
+  # - #getquota: returns the resource usage and limits for a quota root
+  # - #getquotaroot: returns the list of quota roots for a mailbox, as well as
+  #   their resource usage and limits.
+  # - #setquota: sets the resource limits for a given quota root.
+  # - Updates #status with <tt>"DELETED"</tt> and +DELETED-STORAGE+ attributes.
+  #
   # ==== RFC9394: +PARTIAL+
   # - Updates #search, #uid_search with the +PARTIAL+ return option which adds
   #   ESearchResult#partial return data.
@@ -631,9 +651,9 @@
   #   RFC 5322, DOI 10.17487/RFC5322, October 2008,
   #   <https://www.rfc-editor.org/info/rfc5322>.
   #
-  #   <em>Note: obsoletes</em>
-  #   RFC-2822[https://www.rfc-editor.org/rfc/rfc2822]<em> (April 2001) 
and</em>
-  #   RFC-822[https://www.rfc-editor.org/rfc/rfc822]<em> (August 1982).</em>
+  #   *NOTE*: obsoletes
+  #   RFC-2822[https://www.rfc-editor.org/rfc/rfc2822] (April 2001) and
+  #   RFC-822[https://www.rfc-editor.org/rfc/rfc822] (August 1982).
   #
   # [CHARSET[https://www.rfc-editor.org/rfc/rfc2978]]::
   #   Freed, N. and J. Postel, "IANA Charset Registration Procedures", BCP 19,
@@ -698,13 +718,12 @@
   #
   # === \IMAP Extensions
   #
-  # [QUOTA[https://www.rfc-editor.org/rfc/rfc9208]]::
-  #   Melnikov, A., "IMAP QUOTA Extension", RFC 9208, DOI 10.17487/RFC9208,
-  #   March 2022, <https://www.rfc-editor.org/info/rfc9208>.
+  # [QUOTA[https://www.rfc-editor.org/rfc/rfc2087]]::
+  #   Myers, J., "IMAP4 QUOTA extension", RFC 2087, DOI 10.17487/RFC2087,
+  #   January 1997, <https://www.rfc-editor.org/info/rfc2087>.
   #
-  #   <em>Note: obsoletes</em>
-  #   RFC-2087[https://www.rfc-editor.org/rfc/rfc2087]<em> (January 1997)</em>.
-  #   <em>Net::IMAP does not fully support the RFC9208 updates yet.</em>
+  #   *NOTE*: _obsoleted_ by RFC9208[https://www.rfc-editor.org/rfc/rfc9208]
+  #   (March 2022).
   # [IDLE[https://www.rfc-editor.org/rfc/rfc2177]]::
   #   Leiba, B., "IMAP4 IDLE command", RFC 2177, DOI 10.17487/RFC2177,
   #   June 1997, <https://www.rfc-editor.org/info/rfc2177>.
@@ -741,8 +760,8 @@
   #   Gulbrandsen, A. and N. Freed, Ed., "Internet Message Access Protocol
   #   (\IMAP) - MOVE Extension", RFC 6851, DOI 10.17487/RFC6851, January 2013,
   #   <https://www.rfc-editor.org/info/rfc6851>.
-  # [UTF8=ACCEPT[https://www.rfc-editor.org/rfc/rfc6855]]::
-  # [UTF8=ONLY[https://www.rfc-editor.org/rfc/rfc6855]]::
+  # [{UTF8=ACCEPT}[https://www.rfc-editor.org/rfc/rfc6855]]::
+  # [{UTF8=ONLY}[https://www.rfc-editor.org/rfc/rfc6855]]::
   #   Resnick, P., Ed., Newman, C., Ed., and S. Shen, Ed.,
   #   "IMAP Support for UTF-8", RFC 6855, DOI 10.17487/RFC6855, March 2013,
   #   <https://www.rfc-editor.org/info/rfc6855>.
@@ -756,6 +775,11 @@
   #   Gondwana, B., Ed., "IMAP Extension for Object Identifiers",
   #   RFC 8474, DOI 10.17487/RFC8474, September 2018,
   #   <https://www.rfc-editor.org/info/rfc8474>.
+  # [{QUOTA=RES-*}[https://www.rfc-editor.org/rfc/rfc9208]]::
+  #   Melnikov, A., "IMAP QUOTA Extension", RFC 9208, DOI 10.17487/RFC9208,
+  #   March 2022, <https://www.rfc-editor.org/info/rfc9208>.
+  #
+  #   Obsoletes RFC2087[https://www.rfc-editor.org/rfc/rfc2087].
   # [PARTIAL[https://www.rfc-editor.org/info/rfc9394]]::
   #   Melnikov, A., Achuthan, A., Nagulakonda, V., and L. Alves,
   #   "IMAP PARTIAL Extension for Paged SEARCH and FETCH", RFC 9394,
@@ -769,6 +793,7 @@
   #
   # === IANA registries
   # * {IMAP Capabilities}[http://www.iana.org/assignments/imap4-capabilities]
+  #   * {IMAP Quota Resource 
Types}[http://www.iana.org/assignments/imap4-capabilities#imap-capabilities-2]
   # * {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]
@@ -779,8 +804,8 @@
   # * {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]
@@ -788,7 +813,7 @@
   # * {IMAP URLAUTH Authorization Mechanism 
Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml]
   #
   class IMAP < Protocol
-    VERSION = "0.6.3"
+    VERSION = "0.6.4"
 
     # Aliases for supported capabilities, to be used with the #enable command.
     ENABLE_ALIASES = {
@@ -1120,6 +1145,31 @@
       start_imap_connection
     end
 
+    # Returns a string representation of +self+, showing basic client state
+    # information.
+    #
+    #   imap = Net::IMAP.new(hostname, ssl: true)
+    #   imap.inspect #=> "#<Net::IMAP imap.example.net:993 TLS 
not_authenticated>"
+    #
+    #   imap.authenticate(:oauthbearer, "user", token)
+    #   imap.inspect #=> "#<Net::IMAP imap.example.net:993 TLS authenticated>"
+    #
+    #   imap.select("INBOX")
+    #   imap.inspect #=> "#<Net::IMAP imap.example.net:993 TLS selected>"
+    #
+    #   imap.logout
+    #   imap.inspect #=> "#<Net::IMAP imap.example.net:993 TLS logout>"
+    #
+    def inspect
+      tls_state = tls_verified? ? "TLS" :
+        ssl_ctx ? "TLS (NOT VERIFIED)" :
+        "PLAINTEXT"
+      conn_state = disconnected? ? "disconnected" : connection_state.to_sym
+      "#<%s:0x%08x %s:%s %s %s>" % [
+        self.class.name, __id__, host, port, tls_state, conn_state
+      ]
+    end
+
     # Returns true after the TLS negotiation has completed and the remote
     # hostname has been verified.  Returns false when TLS has been established
     # but peer verification was disabled.
@@ -1390,9 +1440,11 @@
     #
     def starttls(**options)
       @ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options)
+      handled = false
       error = nil
       ok = send_command("STARTTLS") do |resp|
         if resp.kind_of?(TaggedResponse) && resp.name == "OK"
+          handled = true
           clear_cached_capabilities
           clear_responses
           start_tls_session
@@ -1404,6 +1456,13 @@
         disconnect
         raise error
       end
+      unless handled
+        disconnect
+        raise InvalidResponseError,
+              "STARTTLS handler was bypassed, although server responded %p" % [
+                ok.raw_data.chomp
+              ]
+      end
       ok
     end
 
@@ -1825,12 +1884,18 @@
     # to both admin and user.  If this mailbox exists, it returns an array
     # containing objects of type MailboxQuotaRoot and MailboxQuota.
     #
+    # *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a 
single
+    # resource type.  This is usually +STORAGE+, but you may need to verify 
this
+    # with UntaggedResponse#raw_data.
+    #
     # Related: #getquota, #setquota, MailboxQuotaRoot, MailboxQuota
     #
     # ==== Capabilities
     #
-    # The server's capabilities must include +QUOTA+
-    # [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]].
+    # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
+    # capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
+    # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
+    # resource type.
     def getquotaroot(mailbox)
       synchronize do
         send_command("GETQUOTAROOT", mailbox)
@@ -1842,41 +1907,59 @@
     end
 
     # Sends a {GETQUOTA command [RFC2087 
§4.2]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.2]
-    # along with specified +mailbox+.  If this mailbox exists, then an array
-    # containing a MailboxQuota object is returned.  This command is generally
-    # only available to server admin.
+    # for the +quota_root+.  If this quota root exists, then an array
+    # containing a MailboxQuota object is returned.
+    #
+    # The names of quota roots that are applicable to a particular mailbox can
+    # be discovered with #getquotaroot.
+    #
+    # *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a 
single
+    # resource type.  This is usually +STORAGE+, but you may need to verify 
this
+    # with UntaggedResponse#raw_data.
     #
     # Related: #getquotaroot, #setquota, MailboxQuota
     #
     # ==== Capabilities
     #
-    # The server's capabilities must include +QUOTA+
-    # [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]].
-    def getquota(mailbox)
+    # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
+    # capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
+    # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
+    # resource type.
+    def getquota(quota_root)
       synchronize do
-        send_command("GETQUOTA", mailbox)
+        send_command("GETQUOTA", quota_root)
         clear_responses("QUOTA")
       end
     end
 
     # Sends a {SETQUOTA command [RFC2087 
§4.1]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.1]
-    # along with the specified +mailbox+ and +quota+.  If +quota+ is nil, then
-    # +quota+ will be unset for that mailbox.  Typically one needs to be logged
-    # in as a server admin for this to work.
+    # along with the specified +quota_root+ and +storage_limit+.  If
+    # +storage_limit+ is +nil+, resource limits are unset for that quota root.
+    # If +storage_limit+ is a number, it sets the +STORAGE+ resource limit.
+    #
+    #   imap.setquota "#user/alice", 100
+    #   imap.getquota "#user/alice"
+    #   # => [#<struct Net::IMAP::MailboxQuota mailbox="#user/alice" usage=54 
quota=100>]
+    #
+    # Typically one needs to be logged in as a server admin for this to work.
+    #
+    # *NOTE:* Currently, Net::IMAP only supports setting +STORAGE+ quota 
limits.
     #
     # Related: #getquota, #getquotaroot
     #
     # ==== Capabilities
     #
-    # The server's capabilities must include +QUOTA+
-    # [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]].
-    def setquota(mailbox, quota)
-      if quota.nil?
-        data = '()'
+    # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
+    # capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
+    # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
+    # resource type.
+    def setquota(quota_root, storage_limit)
+      if storage_limit.nil?
+        list = []
       else
-        data = '(STORAGE ' + quota.to_s + ')'
+        list = ["STORAGE", NumValidator.coerce_number64(storage_limit)]
       end
-      send_command("SETQUOTA", mailbox, RawData.new(data))
+      send_command("SETQUOTA", quota_root, list)
     end
 
     # Sends a {SETACL command [RFC4314 
§3.1]}[https://www.rfc-editor.org/rfc/rfc4314#section-3.1]
@@ -1983,7 +2066,10 @@
     # <tt>STATUS=SIZE</tt>
     # {[RFC8483]}[https://www.rfc-editor.org/rfc/rfc8483.html].
     #
-    # +DELETED+ requires the server's capabilities to include +IMAP4rev2+.
+    # +DELETED+ must be supported when the server's capabilities includes
+    # +IMAP4rev2+.
+    # or <tt>QUOTA=RES-MESSAGES</tt>
+    # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html].
     #
     # +HIGHESTMODSEQ+ requires the server's capabilities to include +CONDSTORE+
     # {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
@@ -2019,6 +2105,11 @@
     #
     # ==== Capabilities
     #
+    # If +BINARY+ [RFC3516[https://www.rfc-editor.org/rfc/rfc3516.html]] is
+    # supported by the server, +message+ may contain +NULL+ characters and
+    # be sent as a binary literal.  Otherwise, binary message parts must be
+    # encoded appropriately (for example, +base64+).
+    #
     # If +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315.html]] is
     # supported and the destination supports persistent UIDs, the server's
     # response should include an +APPENDUID+ response code with AppendUIDData.
@@ -2029,12 +2120,11 @@
     # TODO: add MULTIAPPEND support
     #++
     def append(mailbox, message, flags = nil, date_time = nil)
+      message = StringFormatter.literal_or_literal8(message, name: "message")
       args = []
-      if flags
-        args.push(flags)
-      end
+      args.push(flags)     if flags
       args.push(date_time) if date_time
-      args.push(Literal.new(message))
+      args.push(message)
       send_command("APPEND", mailbox, *args)
     end
 
@@ -2264,11 +2354,11 @@
     #     Encoded as an \IMAP date (see ::encode_date).
     #
     # [When +criteria+ is a String]
-    #   +criteria+ will be sent directly to the server <em>without any
-    #   validation or encoding</em>.
+    #   +criteria+ will be sent to the server <em>with minimal validation and 
no
+    #   encoding or formatting</em>.
     #
-    #   <em>*WARNING:* This is vulnerable to injection attacks when external
-    #   inputs are used.</em>
+    #   <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
+    #   types of attribute injection attack if unvetted user input is 
used.</em>
     #
     # ==== Supported return options
     #
@@ -2589,6 +2679,13 @@
     #
     # +attr+ is a list of attributes to fetch; see FetchStruct documentation 
for
     # a list of supported attributes.
+    # >>>
+    #   When +attr+ is a String, it will be sent <em>with minimal validation 
and
+    #   no encoding or formatting</em>.  When +attr+ is an Array, each String 
in
+    #   +attr+ will be sent this way.
+    #
+    #   <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
+    #   types of attribute injection attack if unvetted user input is 
used.</em>
     #
     # +changedsince+ is an optional integer mod-sequence.  It limits results to
     # messages with a mod-sequence greater than +changedsince+.
@@ -3077,6 +3174,7 @@
 
       synchronize do
         tag = Thread.current[:net_imap_tag] = generate_tag
+        guard_against_tagged_response_skipping_handler!(tag, "IDLE")
         put_string("#{tag} IDLE#{CRLF}")
 
         begin
@@ -3481,7 +3579,7 @@
         raise BadResponseError, resp
       else
         disconnect
-        raise InvalidResponseError, "invalid tagged resp: %p" % 
[resp.raw.chomp]
+        raise InvalidResponseError, "invalid tagged resp: %p" % 
[resp.raw_data.chomp]
       end
     end
 
@@ -3541,21 +3639,29 @@
           put_string(" ")
           send_data(i, tag)
         end
-        put_string(CRLF)
-        if cmd == "LOGOUT"
-          @logout_command_tag = tag
-        end
-        if block
-          add_response_handler(&block)
-        end
+        @logout_command_tag = tag if cmd == "LOGOUT"
+        guard_against_tagged_response_skipping_handler!(tag, cmd)
+        add_response_handler(&block) if block
         begin
-          return get_tagged_response(tag, cmd)
+          put_string(CRLF)
+          get_tagged_response(tag, cmd)
         ensure
-          if block
-            remove_response_handler(block)
-          end
+          remove_response_handler(block) if block
         end
       end
+    rescue InvalidResponseError
+      disconnect
+      raise
+    end
+
+    def guard_against_tagged_response_skipping_handler!(tag, cmd)
+      return unless (resp = @tagged_responses[tag])&.name&.upcase == "OK"
+      raise InvalidResponseError, format(
+        "Received tagged 'OK' to incomplete %s command (tag=%s).  " \
+        "This could indicate a malicious server, a man-in-the-middle, or " \
+        "client-side command injection.  Disconnecting.",
+        cmd, tag
+      )
     end
 
     def generate_tag
@@ -3709,7 +3815,7 @@
     end
 
     def store_internal(cmd, set, attr, flags, unchangedsince: nil)
-      attr = RawData.new(attr) if attr.instance_of?(String)
+      attr = Atom.new(attr) if attr.instance_of?(String)
       args = [SequenceSet.new(set)]
       args << ["UNCHANGEDSINCE", Integer(unchangedsince)] if unchangedsince
       args << attr << flags
@@ -3781,7 +3887,7 @@
         params = (Hash.try_convert(ssl) || {}).freeze
         context = OpenSSL::SSL::SSLContext.new
         context.set_params(params)
-        context.freeze
+        context.setup
         [params, context]
       else
         false
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/metadata new/metadata
--- old/metadata        1980-01-02 01:00:00.000000000 +0100
+++ new/metadata        1980-01-02 01:00:00.000000000 +0100
@@ -1,7 +1,7 @@
 --- !ruby/object:Gem::Specification
 name: net-imap
 version: !ruby/object:Gem::Version
-  version: 0.6.3
+  version: 0.6.4
 platform: ruby
 authors:
 - Shugo Maeda
@@ -46,6 +46,8 @@
 extensions: []
 extra_rdoc_files: []
 files:
+- ".document"
+- ".rdoc_options"
 - BSDL
 - COPYING
 - Gemfile
@@ -129,7 +131,7 @@
     - !ruby/object:Gem::Version
       version: '0'
 requirements: []
-rubygems_version: 4.0.6
+rubygems_version: 4.0.10
 specification_version: 4
 summary: Ruby client api for Internet Message Access Protocol
 test_files: []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rakelib/rdoc.rake new/rakelib/rdoc.rake
--- old/rakelib/rdoc.rake       1980-01-02 01:00:00.000000000 +0100
+++ new/rakelib/rdoc.rake       1980-01-02 01:00:00.000000000 +0100
@@ -12,17 +12,6 @@
       end
     end
 
-    # See https://github.com/ruby/rdoc/pull/936
-    module FixSectionComments
-      def markup(text)
-        @store ||= @parent&.store
-        super
-      end
-      def description; markup comment end
-      def comment;     super || @comments&.first end
-      def parse(_comment_location = nil) super() end
-    end
-
     # render "[label] data" lists as tables.  adapted from "hanna-nouveau" gem.
     module LabelListTable
       def list_item_start(list_item, list_type)
@@ -51,20 +40,14 @@
   prepend RDoc::Generator::NetIMAP::RemoveRedundantParens
 end
 
-class RDoc::Context::Section
-  prepend RDoc::Generator::NetIMAP::FixSectionComments
-end
-
 class RDoc::Markup::ToHtml
   LIST_TYPE_TO_HTML[:NOTE] = ['<table class="rdoc-list note-list"><tbody>', 
'</tbody></table>']
   prepend RDoc::Generator::NetIMAP::LabelListTable
 end
 
 RDoc::Task.new do |doc|
-  doc.main       = "README.md"
   doc.title      = "net-imap #{Net::IMAP::VERSION}"
   doc.rdoc_dir   = "doc"
-  doc.rdoc_files = FileList.new %w[lib/**/*.rb *.rdoc *.md]
   doc.options << "--template-stylesheets" << "docs/styles.css"
-  # doc.generator  = "hanna"
+  doc.generator  = "darkfish" # TODO: fix issues with aliki
 end

Reply via email to