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
