From b3703143768f11b4ab5cf3ad55455c0e81b7082c Mon Sep 17 00:00:00 2001 From: nicholas evans Date: Sun, 12 Nov 2023 07:59:35 -0500 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9C=A8=20Add=20STATUS=20HIGHESTMODSEQ=20?= =?UTF-8?q?(RFC7162,=20CONDSTORE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This doesn't actually require explicit support, because `HIGHESTMODSEQ` is simply another numeric status attribute, and we already parse `number` as the default (without using `ExtensionData`). --- lib/net/imap.rb | 18 ++++++++++++++---- lib/net/imap/response_parser.rb | 5 +++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 869db116..466db06c 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -502,6 +502,10 @@ module Net # # - See #enable for information about support for UTF-8 string encoding. # + # ==== RFC7162: +CONDSTORE+ + # + # - Updates #status with the +HIGHESTMODSEQ+ status attribute. + # # ==== RFC8438: STATUS=SIZE # - Updates #status with the +SIZE+ status attribute. # @@ -1689,7 +1693,7 @@ def lsub(refname, mailbox) end end - # Sends a {STATUS commands [IMAP4rev1 §6.3.10]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.3.10] + # Sends a {STATUS command [IMAP4rev1 §6.3.10]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.3.10] # and returns the status of the indicated +mailbox+. +attr+ is a list of one # or more attributes whose statuses are to be requested. # @@ -1716,10 +1720,13 @@ def lsub(refname, mailbox) # The approximate size of the mailbox---must be greater than or equal to # the sum of all messages' +RFC822.SIZE+ fetch item values. # + # +HIGHESTMODSEQ+:: + # The highest mod-sequence value of all messages in the mailbox. See + # +CONDSTORE+ {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html]. + # # +MAILBOXID+:: - # A server-allocated unique _string_ identifier for the mailbox. - # See +OBJECTID+ - # {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html#section-4]. + # A server-allocated unique _string_ identifier for the mailbox. See + # +OBJECTID+ {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html]. # # +RECENT+:: # The number of messages with the \Recent flag. @@ -1741,6 +1748,9 @@ def lsub(refname, mailbox) # # +DELETED+ requires the server's capabilities to include +IMAP4rev2+. # + # +HIGHESTMODSEQ+ requires the server's capabilities to include +CONDSTORE+ + # {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html]. + # # +MAILBOXID+ requires the server's capabilities to include +OBJECTID+ # {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html]. def status(mailbox, attr) diff --git a/lib/net/imap/response_parser.rb b/lib/net/imap/response_parser.rb index 138b8d04..3318e98b 100644 --- a/lib/net/imap/response_parser.rb +++ b/lib/net/imap/response_parser.rb @@ -1594,6 +1594,7 @@ def status_att_val when "UIDVALIDITY" then nz_number # RFC3501, RFC9051 when "RECENT" then number # RFC3501 (obsolete) when "SIZE" then number64 # RFC8483, RFC9051 + when "HIGHESTMODSEQ" then mod_sequence_valzer # RFC7162 when "MAILBOXID" then parens__objectid # RFC8474 else number? || ExtensionData.new(tagged_ext_val) @@ -1966,6 +1967,10 @@ def charset; quoted? || atom end # ;; Per-message mod-sequence. alias permsg_modsequence mod_sequence_value + # RFC7162: + # mod-sequence-valzer = "0" / mod-sequence-value + alias mod_sequence_valzer number64 + def parens__modseq; lpar; _ = permsg_modsequence; rpar; _ end # RFC8474: From 5f39345cc9780be58c35930f8e26530eb25caeeb Mon Sep 17 00:00:00 2001 From: nicholas evans Date: Sat, 18 Feb 2023 23:39:31 -0500 Subject: [PATCH 2/8] =?UTF-8?q?=E2=9C=A8=20CONDSTORE:=20return=20SearchRes?= =?UTF-8?q?ult=20with=20#modseq?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, MODSEQ was parsed but ignored. SearchResult inherits from Array, to preserve backwards compatibility. --- lib/net/imap.rb | 21 +++ lib/net/imap/response_data.rb | 1 + lib/net/imap/response_parser.rb | 3 +- lib/net/imap/search_result.rb | 150 ++++++++++++++++++ .../response_parser/search_responses.yml | 49 +++--- test/net/imap/test_search_result.rb | 96 +++++++++++ 6 files changed, 294 insertions(+), 26 deletions(-) create mode 100644 lib/net/imap/search_result.rb create mode 100644 test/net/imap/test_search_result.rb diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 466db06c..fd24a8e5 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -505,6 +505,10 @@ module Net # ==== RFC7162: +CONDSTORE+ # # - Updates #status with the +HIGHESTMODSEQ+ status attribute. + # - Updates #search, #uid_search, #sort, and #uid_sort with the +MODSEQ+ + # search criterion, and adds SearchResult#modseq to the search response. + # - Updates #thread and #uid_thread with the +MODSEQ+ search criterion + # (but thread responses are unchanged). # # ==== RFC8438: STATUS=SIZE # - Updates #status with the +SIZE+ status attribute. @@ -1887,6 +1891,10 @@ def uid_expunge(uid_set) # string holding the entire search string, or a single-dimension array of # search keywords and arguments. # + # Returns a SearchResult object. SearchResult inherits from Array (for + # backward compatibility) but adds SearchResult#modseq when the +CONDSTORE+ + # capability has been enabled. + # # Related: #uid_search # # ===== Search criteria @@ -1935,6 +1943,15 @@ def uid_expunge(uid_set) # p imap.search(["SUBJECT", "hello", "NOT", "NEW"]) # #=> [1, 6, 7, 8] # + # ===== Capabilities + # + # If [CONDSTORE[https://www.rfc-editor.org/rfc/rfc7162.html]] is supported + # and enabled for the selected mailbox, a non-empty SearchResult will + # include a +MODSEQ+ value. + # imap.select("mbox", condstore: true) + # result = imap.search(["SUBJECT", "hi there", "not", "new") + # #=> Net::IMAP::SearchResult[1, 6, 7, 8, modseq: 5594] + # result.modseq # => 5594 def search(keys, charset = nil) return search_internal("SEARCH", keys, charset) end @@ -1943,6 +1960,10 @@ def search(keys, charset = nil) # to search the mailbox for messages that match the given searching # criteria, and returns unique identifiers (UIDs). # + # Returns a SearchResult object. SearchResult inherits from Array (for + # backward compatibility) but adds SearchResult#modseq when the +CONDSTORE+ + # capability has been enabled. + # # See #search for documentation of search criteria. def uid_search(keys, charset = nil) return search_internal("UID SEARCH", keys, charset) diff --git a/lib/net/imap/response_data.rb b/lib/net/imap/response_data.rb index daeb0957..f8493bce 100644 --- a/lib/net/imap/response_data.rb +++ b/lib/net/imap/response_data.rb @@ -3,6 +3,7 @@ module Net class IMAP < Protocol autoload :FetchData, "#{__dir__}/fetch_data" + autoload :SearchResult, "#{__dir__}/search_result" autoload :SequenceSet, "#{__dir__}/sequence_set" # Net::IMAP::ContinuationRequest represents command continuation requests. diff --git a/lib/net/imap/response_parser.rb b/lib/net/imap/response_parser.rb index 3318e98b..a6ee4ca3 100644 --- a/lib/net/imap/response_parser.rb +++ b/lib/net/imap/response_parser.rb @@ -1467,9 +1467,10 @@ def mailbox_data__search while _ = SP? && nz_number? do data << _ end if lpar? label("MODSEQ"); SP! - mod_sequence_value + modseq = mod_sequence_value rpar end + data = SearchResult.new(data, modseq: modseq) UntaggedResponse.new(name, data, @str) end alias sort_data mailbox_data__search diff --git a/lib/net/imap/search_result.rb b/lib/net/imap/search_result.rb new file mode 100644 index 00000000..3b871191 --- /dev/null +++ b/lib/net/imap/search_result.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +module Net + class IMAP + + # An array of sequence numbers returned by Net::IMAP#search, or unique + # identifiers returned by Net::IMAP#uid_search. + # + # For backward compatibility, SearchResult inherits from Array. + class SearchResult < Array + + # Returns a frozen SearchResult populated with the given +seq_nums+. + # + # Net::IMAP::SearchResult[1, 3, 5, modseq: 9] + # # => Net::IMAP::SearchResult[1, 3, 5, modseq: 9] + def self.[](*seq_nums, modseq: nil) + new(seq_nums, modseq: modseq) + end + + # A modification sequence number, as described by the +CONDSTORE+ + # extension in {[RFC7162 + # §3.1.6]}[https://www.rfc-editor.org/rfc/rfc7162.html#section-3.1.6]. + attr_reader :modseq + + # Returns a frozen SearchResult populated with the given +seq_nums+. + # + # Net::IMAP::SearchResult.new([1, 3, 5], modseq: 9) + # # => Net::IMAP::SearchResult[1, 3, 5, modseq: 9] + def initialize(seq_nums, modseq: nil) + super(seq_nums.to_ary.map { Integer _1 }) + @modseq = Integer modseq if modseq + freeze + end + + # Returns a frozen copy of +other+. + def initialize_copy(other); super; freeze end + + # Returns whether +other+ is a SearchResult with the same values and the + # same #modseq. The order of numbers is irrelevant. + # + # Net::IMAP::SearchResult[123, 456, modseq: 789] == + # Net::IMAP::SearchResult[123, 456, modseq: 789] + # # => true + # Net::IMAP::SearchResult[123, 456, modseq: 789] == + # Net::IMAP::SearchResult[456, 123, modseq: 789] + # # => true + # + # Net::IMAP::SearchResult[123, 456, modseq: 789] == + # Net::IMAP::SearchResult[987, 654, modseq: 789] + # # => false + # Net::IMAP::SearchResult[123, 456, modseq: 789] == + # Net::IMAP::SearchResult[1, 2, 3, modseq: 9999] + # # => false + # + # SearchResult can be compared directly with Array, if #modseq is nil and + # the array is sorted. + # + # Net::IMAP::SearchResult[9, 8, 6, 4, 1] == [1, 4, 6, 8, 9] # => true + # Net::IMAP::SearchResult[3, 5, 7, modseq: 99] == [3, 5, 7] # => false + # + # Note that Array#== does require matching order and ignores #modseq. + # + # [9, 8, 6, 4, 1] == Net::IMAP::SearchResult[1, 4, 6, 8, 9] # => false + # [3, 5, 7] == Net::IMAP::SearchResult[3, 5, 7, modseq: 99] # => true + # + def ==(other) + (modseq ? + other.is_a?(self.class) && modseq == other.modseq : + other.is_a?(Array)) && + size == other.size && + sort == other.sort + end + + # Hash equality. Unlike #==, order will be taken into account. + def hash + return super if modseq.nil? + [super, self.class, modseq].hash + end + + # Hash equality. Unlike #==, order will be taken into account. + def eql?(other) + return super if modseq.nil? + self.class == other.class && hash == other.hash + end + + # Returns a string that represents the SearchResult. + # + # Net::IMAP::SearchResult[123, 456, 789].inspect + # # => "[123, 456, 789]" + # + # Net::IMAP::SearchResult[543, 210, 678, modseq: 2048].inspect + # # => "Net::IMAP::SearchResult[543, 210, 678, modseq: 2048]" + # + def inspect + return super if modseq.nil? + "%s[%s, modseq: %p]" % [self.class, join(", "), modseq] + end + + # Returns a string that follows the formal \IMAP syntax. + # + # data = Net::IMAP::SearchResult[2, 8, 32, 128, 256, 512] + # data.to_s # => "* SEARCH 2 8 32 128 256 512" + # data.to_s("SEARCH") # => "* SEARCH 2 8 32 128 256 512" + # data.to_s("SORT") # => "* SORT 2 8 32 128 256 512" + # data.to_s(nil) # => "2 8 32 128 256 512" + # + # data = Net::IMAP::SearchResult[1, 3, 16, 1024, modseq: 2048].to_s + # data.to_s # => "* SEARCH 1 3 16 1024 (MODSEQ 2048)" + # data.to_s("SORT") # => "* SORT 1 3 16 1024 (MODSEQ 2048)" + # data.to_s # => "1 3 16 1024 (MODSEQ 2048)" + # + def to_s(type = "SEARCH") + str = +"" + str << "* %s " % [type.to_str] unless type.nil? + str << join(" ") + str << " (MODSEQ %d)" % [modseq] if modseq + -str + end + + # Converts the SearchResult into a SequenceSet. + # + # 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 + + def pretty_print(pp) + return super if modseq.nil? + pp.text self.class.name + "[" + pp.group_sub do + pp.nest(2) do + pp.breakable "" + each do |num| + pp.pp num + pp.text "," + pp.fill_breakable + end + pp.breakable "" + pp.text "modseq: " + pp.pp modseq + end + pp.breakable "" + pp.text "]" + end + end + + end + + end +end diff --git a/test/net/imap/fixtures/response_parser/search_responses.yml b/test/net/imap/fixtures/response_parser/search_responses.yml index 714adb1e..d94917e3 100644 --- a/test/net/imap/fixtures/response_parser/search_responses.yml +++ b/test/net/imap/fixtures/response_parser/search_responses.yml @@ -17,35 +17,34 @@ :response: "* SEARCH\r\n" :expected: !ruby/struct:Net::IMAP::UntaggedResponse name: SEARCH - data: [] - # data: !ruby/array:Net::IMAP::SearchResult - # internal: [] - # ivars: - # "@modseq": + data: !ruby/array:Net::IMAP::SearchResult + internal: [] + ivars: + "@modseq": raw_data: "* SEARCH\r\n" test_search_response_single_seq_nums_returned: :response: "* SEARCH 1\r\n" :expected: !ruby/struct:Net::IMAP::UntaggedResponse name: SEARCH - data: # !ruby/array:Net::IMAP::SearchResult - # internal: + data: !ruby/array:Net::IMAP::SearchResult + internal: - 1 - # ivars: - # "@modseq": + ivars: + "@modseq": raw_data: "* SEARCH 1\r\n" test_search_response_multiple_seq_nums_returned: :response: "* SEARCH 1 2 3\r\n" :expected: !ruby/struct:Net::IMAP::UntaggedResponse name: SEARCH - data: # !ruby/array:Net::IMAP::SearchResult - # internal: + data: !ruby/array:Net::IMAP::SearchResult + internal: - 1 - 2 - 3 - # ivars: - # "@modseq": + ivars: + "@modseq": raw_data: "* SEARCH 1 2 3\r\n" test_invalid_search_response_single_result_with_trailing_space: @@ -53,11 +52,11 @@ :response: "* SEARCH 1 \r\n" :expected: !ruby/struct:Net::IMAP::UntaggedResponse name: SEARCH - data: # !ruby/array:Net::IMAP::SearchResult - # internal: + data: !ruby/array:Net::IMAP::SearchResult + internal: - 1 - # ivars: - # "@modseq": + ivars: + "@modseq": raw_data: "* SEARCH 1 \r\n" test_invalid_search_response_multiple_result_with_trailing_space: @@ -65,13 +64,13 @@ :response: "* SEARCH 1 2 3 \r\n" :expected: !ruby/struct:Net::IMAP::UntaggedResponse name: SEARCH - data: # !ruby/array:Net::IMAP::SearchResult - # internal: + data: !ruby/array:Net::IMAP::SearchResult + internal: - 1 - 2 - 3 - # ivars: - # "@modseq": + ivars: + "@modseq": raw_data: "* SEARCH 1 2 3 \r\n" test_search_response_with_condstore_modseq: @@ -80,10 +79,10 @@ :response: "* SEARCH 87216 87221 (MODSEQ 7667567)\r\n" :expected: !ruby/struct:Net::IMAP::UntaggedResponse name: SEARCH - data: # !ruby/array:Net::IMAP::SearchResult - # internal: + data: !ruby/array:Net::IMAP::SearchResult + internal: - 87216 - 87221 - # ivars: - # "@modseq": 7667567 + ivars: + "@modseq": 7667567 raw_data: "* SEARCH 87216 87221 (MODSEQ 7667567)\r\n" diff --git a/test/net/imap/test_search_result.rb b/test/net/imap/test_search_result.rb new file mode 100644 index 00000000..e048b93f --- /dev/null +++ b/test/net/imap/test_search_result.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require "net/imap" +require "test/unit" + +class SearchDataTests < Test::Unit::TestCase + SearchResult = Net::IMAP::SearchResult + + test "#frozen?" do + assert SearchResult.new([1, 3, 5]).frozen? + assert SearchResult[1, 3, 5].frozen? + assert SearchResult[1, 3, 5, modseq: 9].frozen? + assert SearchResult[1, 3, 5, modseq: 9].clone.frozen? + assert SearchResult[1, 3, 5, modseq: 9].dup.dup.frozen? + end + + test "#modseq" do + assert_nil SearchResult[12, 34].modseq + assert_equal 123_456_789, SearchResult[12, 34, modseq: 123_456_789].modseq + end + + test "#== ignores the order of elements" do + unsorted = SearchResult[4, 2, 2048, 99] + sorted = SearchResult[2, 4, 99, 2048] + array = [2, 4, 99, 2048] + assert_equal sorted, array + assert_equal unsorted, array + end + + test "#== checks modseq" do + unsorted = SearchResult[4, 2, 2048, 99, modseq: 99_999] + sorted = SearchResult[2, 4, 99, 2048, modseq: 99_999] + assert_equal unsorted, sorted + assert_equal sorted, unsorted + end + + test "SearchResult[*nz_numbers] == Array[*nz_numbers]" do + array = [1, 5, 20, 3, 98] + result = SearchResult[*array] + assert_equal array, result + assert_equal result, array + end + + test "SearchResult.new(nz_numbers) == Array.new(nz_numbers)" do + nz_numbers = [11, 35, 39, 1083, 958] + result = SearchResult.new(nz_numbers) + array = Array.new(nz_numbers) + assert_equal array, result + assert_equal result, array + end + + test "SearchResult[*nz_numbers, modseq: nz_number] != Array[*nz_numbers]" do + array = [1, 5, 20, 3, 98] + result = SearchResult[*array, modseq: 123456] + refute_equal result, array + end + + test "Array[*nz_numbers] == SearchResult[*nz_numbers, modseq: nz_number]" do + array = [1, 5, 20, 3, 98] + result = SearchResult[*array, modseq: 123456] + assert_equal array, result + end + + test "SearchResult[*nz_numbers] == Array[*differently_sorted]" do + array = [1, 5, 20, 3, 98] + result = SearchResult[*array.reverse] + assert_equal result, array + end + + test "Array[*nz_numbers] != SearchResult[*differently_sorted]" do + array = [1, 5, 20, 3, 98] + result = SearchResult[*array.reverse] + refute_equal array, result + end + + test "#inspect" do + assert_equal "[1, 2, 3]", Net::IMAP::SearchResult[1, 2, 3].inspect + assert_equal("Net::IMAP::SearchResult[1, 3, modseq: 9]", + Net::IMAP::SearchResult[1, 3, modseq: 9].inspect) + end + + test "#to_s" do + assert_equal "* SEARCH 1 2 3", Net::IMAP::SearchResult[1, 2, 3].to_s + assert_equal("* SEARCH 3 2 1 (MODSEQ 9)", + Net::IMAP::SearchResult[3, 2, 1, modseq: 9].to_s) + end + + test "#to_s(type)" do + assert_equal "* SEARCH 1 3", Net::IMAP::SearchResult[1, 3].to_s("SEARCH") + assert_equal "* SORT 1 2 3", Net::IMAP::SearchResult[1, 2, 3].to_s("SORT") + assert_equal("* SORT 99 111 44 (MODSEQ 999)", + Net::IMAP::SearchResult[99, 111, 44, modseq: 999].to_s("SORT")) + assert_equal("99 111 44 (MODSEQ 999)", + Net::IMAP::SearchResult[99, 111, 44, modseq: 999].to_s(nil)) + end +end From a29563f20e6d203feb855863a117d7e937b99d0e Mon Sep 17 00:00:00 2001 From: nicholas evans Date: Sun, 22 Jan 2023 17:29:31 -0500 Subject: [PATCH 3/8] =?UTF-8?q?=E2=9C=A8=20Parse=20all=20CONDSTORE=20respo?= =?UTF-8?q?nse=20codes=20(RFC7162)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * `NOMODSEQ` _(already previously supported)_ * `HIGHESTMODSEQ` * `MODIFIED` --- lib/net/imap/response_data.rb | 10 ++++++++++ lib/net/imap/response_parser.rb | 6 +++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/net/imap/response_data.rb b/lib/net/imap/response_data.rb index f8493bce..b2a7c24c 100644 --- a/lib/net/imap/response_data.rb +++ b/lib/net/imap/response_data.rb @@ -292,6 +292,16 @@ class ResponseText < Struct.new(:code, :text) # because the server doesn't allow deletion of mailboxes with children. # #data is +nil+. # + # ==== +CONDSTORE+ extension + # See {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html]. + # * +NOMODSEQ+, when selecting a mailbox that does not support + # mod-sequences. #data is +nil+. See IMAP#select. + # * +HIGHESTMODSEQ+, #data is an Integer, the highest mod-sequence value of + # all messages in the mailbox. See IMAP#select. + # * +MODIFIED+, #data is a SequenceSet, the messages that have been modified + # since the +UNCHANGEDSINCE+ mod-sequence given to +STORE+ or UID + # STORE. + # # ==== +OBJECTID+ extension # See {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html]. # * +MAILBOXID+, #data is a string diff --git a/lib/net/imap/response_parser.rb b/lib/net/imap/response_parser.rb index a6ee4ca3..5bd672f4 100644 --- a/lib/net/imap/response_parser.rb +++ b/lib/net/imap/response_parser.rb @@ -1802,6 +1802,8 @@ def resp_text # resp-text-code =/ "HIGHESTMODSEQ" SP mod-sequence-value / # "NOMODSEQ" / # "MODIFIED" SP sequence-set + # RFC7162 (QRESYNC): + # resp-text-code =/ "CLOSED" # # RFC8474: OBJECTID # resp-text-code =/ "MAILBOXID" SP "(" objectid ")" @@ -1823,7 +1825,9 @@ def resp_text_code "EXPUNGEISSUED", "CORRUPTION", "SERVERBUG", "CLIENTBUG", "CANNOT", "LIMIT", "OVERQUOTA", "ALREADYEXISTS", "NONEXISTENT", "CLOSED", "NOTSAVED", "UIDNOTSTICKY", "UNKNOWN-CTE", "HASCHILDREN" - when "NOMODSEQ" # CONDSTORE + when "NOMODSEQ" then nil # CONDSTORE + when "HIGHESTMODSEQ" then SP!; mod_sequence_value # CONDSTORE + when "MODIFIED" then SP!; sequence_set # CONDSTORE when "MAILBOXID" then SP!; parens__objectid # RFC8474: OBJECTID else SP? and text_chars_except_rbra From 207fe5f95572a3f6cd2cd20466b9f13b1c4686ea Mon Sep 17 00:00:00 2001 From: nicholas evans Date: Wed, 22 Nov 2023 09:22:29 -0500 Subject: [PATCH 4/8] =?UTF-8?q?=E2=9C=85=20Add=20RFC7162=20example=20respo?= =?UTF-8?q?nses=20to=20parser=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note that QRESYNC isn't actually supported yet, so `VANISHED` responses are parsed as `response_data__unhandled`, which simply returns a string wrapped in `UnparsedData`. --- .../rfc7162_condstore_qresync_responses.yml | 119 ++++++++++++++++++ test/net/imap/test_imap_response_parser.rb | 3 + 2 files changed, 122 insertions(+) create mode 100644 test/net/imap/fixtures/response_parser/rfc7162_condstore_qresync_responses.yml diff --git a/test/net/imap/fixtures/response_parser/rfc7162_condstore_qresync_responses.yml b/test/net/imap/fixtures/response_parser/rfc7162_condstore_qresync_responses.yml new file mode 100644 index 00000000..dc8fadcf --- /dev/null +++ b/test/net/imap/fixtures/response_parser/rfc7162_condstore_qresync_responses.yml @@ -0,0 +1,119 @@ +--- +:tests: + + "RFC7162 CONDSTORE 3.1.2.1. HIGHESTMODSEQ Response Code": + :response: "* OK [HIGHESTMODSEQ 715194045007]\r\n" + :expected: !ruby/struct:Net::IMAP::UntaggedResponse + name: OK + data: !ruby/struct:Net::IMAP::ResponseText + code: !ruby/struct:Net::IMAP::ResponseCode + name: HIGHESTMODSEQ + data: 715194045007 + text: '' + raw_data: "* OK [HIGHESTMODSEQ 715194045007]\r\n" + + "RFC7162 CONDSTORE 3.1.2.2. NOMODSEQ Response Code": + :response: "* OK [NOMODSEQ] Sorry, this mailbox format doesn't support + modsequences\r\n" + :expected: !ruby/struct:Net::IMAP::UntaggedResponse + name: OK + data: !ruby/struct:Net::IMAP::ResponseText + code: !ruby/struct:Net::IMAP::ResponseCode + name: NOMODSEQ + data: + text: Sorry, this mailbox format doesn't support modsequences + raw_data: "* OK [NOMODSEQ] Sorry, this mailbox format doesn't support + modsequences\r\n" + + "RFC7162 CONDSTORE 3.1.3. Example 3 (FETCH MODSEQ)": + :response: "* 4 FETCH (UID 8 MODSEQ (12121230956))\r\n" + :expected: !ruby/struct:Net::IMAP::UntaggedResponse + name: FETCH + data: !ruby/struct:Net::IMAP::FetchData + seqno: 4 + attr: + UID: 8 + MODSEQ: 12121230956 + raw_data: "* 4 FETCH (UID 8 MODSEQ (12121230956))\r\n" + + "RFC7162 CONDSTORE 3.1.3. Example 4 (FETCH MODSEQ)": + :response: "* 50 FETCH (MODSEQ (12111230048))\r\n" + :expected: !ruby/struct:Net::IMAP::UntaggedResponse + name: FETCH + data: !ruby/struct:Net::IMAP::FetchData + seqno: 50 + attr: + MODSEQ: 12111230048 + raw_data: "* 50 FETCH (MODSEQ (12111230048))\r\n" + + "RFC7162 CONDSTORE 3.1.3. Example 6 (MODIFIED Response Code)": + :response: "d105 OK [MODIFIED 7,9] Conditional STORE failed\r\n" + :expected: !ruby/struct:Net::IMAP::TaggedResponse + tag: d105 + name: OK + data: !ruby/struct:Net::IMAP::ResponseText + code: !ruby/struct:Net::IMAP::ResponseCode + name: MODIFIED + data: !ruby/object:Net::IMAP::SequenceSet + str: '7,9' + tuples: + - - 7 + - 7 + - - 9 + - 9 + text: Conditional STORE failed + raw_data: "d105 OK [MODIFIED 7,9] Conditional STORE failed\r\n" + + "RFC7162 CONDSTORE 3.1.5. MODSEQ Search Criterion in SEARCH": + :response: "* SEARCH 2 5 6 7 11 12 18 19 20 23 (MODSEQ 917162500)\r\n" + :expected: !ruby/struct:Net::IMAP::UntaggedResponse + name: SEARCH + data: !ruby/array:Net::IMAP::SearchResult + internal: + - 2 + - 5 + - 6 + - 7 + - 11 + - 12 + - 18 + - 19 + - 20 + - 23 + ivars: + :@modseq: 917162500 + raw_data: "* SEARCH 2 5 6 7 11 12 18 19 20 23 (MODSEQ 917162500)\r\n" + + "RFC7162 CONDSTORE 3.1.7. HIGHESTMODSEQ Status Data Items": + :response: "* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292 HIGHESTMODSEQ + 7011231777)\r\n" + :expected: !ruby/struct:Net::IMAP::UntaggedResponse + name: STATUS + data: !ruby/struct:Net::IMAP::StatusData + mailbox: blurdybloop + attr: + MESSAGES: 231 + UIDNEXT: 44292 + HIGHESTMODSEQ: 7011231777 + raw_data: "* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292 HIGHESTMODSEQ + 7011231777)\r\n" + + "RFC7162 QRESYNC 3.2.5.1. Modification Sequence and UID Parameters": + :response: "* VANISHED (EARLIER) 41,43:116,118,120:211,214:540\r\n" + :expected: !ruby/struct:Net::IMAP::UntaggedResponse + name: VANISHED + data: !ruby/struct:Net::IMAP::UnparsedData + unparsed_data: "(EARLIER) 41,43:116,118,120:211,214:540" + raw_data: "* VANISHED (EARLIER) 41,43:116,118,120:211,214:540\r\n" + comment: | + Note that QRESYNC isn't supported yet, so the data is unparsed. + + "RFC7162 QRESYNC 3.2.7. EXPUNGE Command": + :response: "* VANISHED 405,407,410,425\r\n" + :expected: !ruby/struct:Net::IMAP::UntaggedResponse + name: VANISHED + data: !ruby/struct:Net::IMAP::UnparsedData + unparsed_data: '405,407,410,425' + raw_data: "* VANISHED 405,407,410,425\r\n" + comment: | + Note that QRESYNC isn't supported yet, so the data is unparsed. diff --git a/test/net/imap/test_imap_response_parser.rb b/test/net/imap/test_imap_response_parser.rb index 9644d749..0d4682cd 100644 --- a/test/net/imap/test_imap_response_parser.rb +++ b/test/net/imap/test_imap_response_parser.rb @@ -90,6 +90,9 @@ def teardown # RFC 5256: THREAD response generate_tests_from fixture_file: "thread_responses.yml" + # RFC 7164: CONDSTORE and QRESYNC responses + generate_tests_from fixture_file: "rfc7162_condstore_qresync_responses.yml" + # RFC 8474: OBJECTID responses generate_tests_from fixture_file: "rfc8474_objectid_responses.yml" From 647a7456e9bbf0acb733b4cdd7a6cf1d218507df Mon Sep 17 00:00:00 2001 From: nicholas evans Date: Fri, 24 Nov 2023 15:36:57 -0500 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9C=A8=20Add=20`condstore`=20kwarg=20to?= =?UTF-8?q?=20`#select`,=20`#examine`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #122. --- lib/net/imap.rb | 25 +++++++++++++++++---- test/net/imap/fake_server/command_reader.rb | 2 +- test/net/imap/fake_server/command_router.rb | 10 ++++++--- test/net/imap/test_imap.rb | 16 +++++++++++++ 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index fd24a8e5..4f53e8da 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -505,6 +505,8 @@ module Net # ==== RFC7162: +CONDSTORE+ # # - Updates #status with the +HIGHESTMODSEQ+ status attribute. + # - Updates #select and #examine with the +condstore+ modifier, and adds + # either a +HIGHESTMODSEQ+ or +NOMODSEQ+ ResponseCode to the responses. # - Updates #search, #uid_search, #sort, and #uid_sort with the +MODSEQ+ # search criterion, and adds SearchResult#modseq to the search response. # - Updates #thread and #uid_thread with the +MODSEQ+ search criterion @@ -1353,6 +1355,12 @@ def login(user, password) # or when existing messages are expunged; see #add_response_handler for a # way to detect these events. # + # When the +condstore+ keyword argument is true, the server is told to + # enable the extension. If +mailbox+ supports persistence of mod-sequences, + # the +HIGHESTMODSEQ+ ResponseCode will be sent as an untagged response to + # #select and all `FETCH` responses will include FetchData#modseq. + # Otherwise, the +NOMODSEQ+ ResponseCode will be sent. + # # A Net::IMAP::NoResponseError is raised if the mailbox does not # exist or is for some reason non-selectable. # @@ -1365,10 +1373,17 @@ def login(user, password) # response code indicating that the mailstore does not support persistent # UIDs: # imap.responses("NO", &:last)&.code&.name == "UIDNOTSTICKY" - def select(mailbox) + # + # If [CONDSTORE[https://www.rfc-editor.org/rfc/rfc7162.html]] is supported, + # the +condstore+ keyword parameter may be used. + # imap.select("mbox", condstore: true) + # modseq = imap.responses("HIGHESTMODSEQ", &:last) + def select(mailbox, condstore: false) + args = ["SELECT", mailbox] + args << ["CONDSTORE"] if condstore synchronize do @responses.clear - send_command("SELECT", mailbox) + send_command(*args) end end @@ -1381,10 +1396,12 @@ def select(mailbox) # exist or is for some reason non-examinable. # # Related: #select - def examine(mailbox) + def examine(mailbox, condstore: false) + args = ["EXAMINE", mailbox] + args << ["CONDSTORE"] if condstore synchronize do @responses.clear - send_command("EXAMINE", mailbox) + send_command(*args) end end diff --git a/test/net/imap/fake_server/command_reader.rb b/test/net/imap/fake_server/command_reader.rb index ecfef530..b840384f 100644 --- a/test/net/imap/fake_server/command_reader.rb +++ b/test/net/imap/fake_server/command_reader.rb @@ -33,7 +33,7 @@ def parse(buf) /\A([^ ]+) ((?:UID )?\w+)(?: (.+))?\r\n\z/min =~ buf or raise "bad request: %p" [buf] case $2.upcase - when "LOGIN", "SELECT", "ENABLE", "AUTHENTICATE" + when "LOGIN", "SELECT", "EXAMINE", "ENABLE", "AUTHENTICATE" Command.new $1, $2, scan_astrings($3), buf else Command.new $1, $2, $3, buf # TODO... diff --git a/test/net/imap/fake_server/command_router.rb b/test/net/imap/fake_server/command_router.rb index 69ad97de..82430316 100644 --- a/test/net/imap/fake_server/command_router.rb +++ b/test/net/imap/fake_server/command_router.rb @@ -132,13 +132,13 @@ def handler_for(command) permanentflags: %i[Deleted Seen *].freeze, }.freeze - on "SELECT" do |resp| + def select_handler(command, resp) state.user or return resp.fail_bad_state(state) name, args = resp.args name or return resp.fail_bad_args name = name.upcase if name.to_s.casecmp? "inbox" mbox = config.mailboxes[name] - mbox or return resp.fail_no "invalid mailbox" + mbox or return resp.fail_no "invalid mailbox %p" % [name] state.select mbox: mbox, args: args attrs = RFC3501_6_3_1_SELECT_EXAMPLE_DATA.merge mbox.to_h resp.untagged "%{exists} EXISTS" % attrs @@ -153,9 +153,13 @@ def handler_for(command) resp.untagged "OK [PERMANENTFLAGS (%s)] Limited" % [ flags(attrs[:permanentflags]) ] - resp.done_ok code: "READ-WRITE" + code = command == "SELECT" ? "READ-WRITE" : "READ-ONLY" + resp.done_ok code: code end + on "SELECT" do |resp| select_handler "SELECT", resp end + on "EXAMINE" do |resp| select_handler "EXAMINE", resp end + on "CLOSE", "UNSELECT" do |resp| resp.args.nil? or return resp.fail_bad_args state.unselect diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index a313939c..3ce5695b 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -1135,6 +1135,22 @@ def test_clear_responses end end + test "#select with condstore" do + with_fake_server do |server, imap| + imap.select "inbox", condstore: true + assert_equal("RUBY0001 SELECT inbox (CONDSTORE)", + server.commands.pop.raw.strip) + end + end + + test "#examine with condstore" do + with_fake_server do |server, imap| + imap.examine "inbox", condstore: true + assert_equal("RUBY0001 EXAMINE inbox (CONDSTORE)", + server.commands.pop.raw.strip) + end + end + def test_close with_fake_server(select: "inbox") do |server, imap| resp = imap.close From 3d2493d50277fe2cee28e4f10a30d1f0c137e380 Mon Sep 17 00:00:00 2001 From: nicholas evans Date: Fri, 24 Nov 2023 15:42:20 -0500 Subject: [PATCH 6/8] =?UTF-8?q?=E2=9C=A8=20Add=20`changedsince`=20kwarg=20?= =?UTF-8?q?to=20`#fetch`,=20`#uid=5Ffetch`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #132. --- lib/net/imap.rb | 38 +++++++++++++++++++++++++++++++++----- test/net/imap/test_imap.rb | 18 ++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 4f53e8da..30451697 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -511,6 +511,8 @@ module Net # search criterion, and adds SearchResult#modseq to the search response. # - Updates #thread and #uid_thread with the +MODSEQ+ search criterion # (but thread responses are unchanged). + # - Updates #fetch and #uid_fetch with the +changedsince+ modifier and + # +MODSEQ+ FetchData attribute. # # ==== RFC8438: STATUS=SIZE # - Updates #status with the +SIZE+ status attribute. @@ -1986,6 +1988,9 @@ def uid_search(keys, charset = nil) return search_internal("UID SEARCH", keys, charset) end + # :call-seq: + # fetch(set, attr, changedsince: nil) -> array of FetchData + # # Sends a {FETCH command [IMAP4rev1 §6.4.5]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.5] # to retrieve data associated with a message in the mailbox. # @@ -2001,6 +2006,9 @@ def uid_search(keys, charset = nil) # +attr+ is a list of attributes to fetch; see the documentation # for FetchData for a list of valid attributes. # + # +changedsince+ is an optional integer mod-sequence. It limits results to + # messages with a mod-sequence greater than +changedsince+. + # # The return value is an array of FetchData. # # Related: #uid_search, FetchData @@ -2022,10 +2030,23 @@ def uid_search(keys, charset = nil) # #=> "12-Oct-2000 22:40:59 +0900" # p data.attr["UID"] # #=> 98 - def fetch(set, attr, mod = nil) - return fetch_internal("FETCH", set, attr, mod) + # + # ===== Capabilities + # + # Many extensions define new message +attr+ names. See FetchData for a list + # of supported extension fields. + # + # The server's capabilities must include +CONDSTORE+ + # {[RFC7162]}[https://tools.ietf.org/html/rfc7162] in order to use the + # +changedsince+ argument. Using +changedsince+ implicitly enables the + # +CONDSTORE+ extension. + def fetch(set, attr, mod = nil, changedsince: nil) + fetch_internal("FETCH", set, attr, mod, changedsince: changedsince) end + # :call-seq: + # uid_fetch(set, attr, changedsince: nil) -> array of FetchData + # # Sends a {UID FETCH command [IMAP4rev1 §6.4.8]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.8] # to retrieve data associated with a message in the mailbox. # @@ -2038,8 +2059,11 @@ def fetch(set, attr, mod = nil) # whether a +UID+ was specified as a message data item to the +FETCH+. # # Related: #fetch, FetchData - def uid_fetch(set, attr, mod = nil) - return fetch_internal("UID FETCH", set, attr, mod) + # + # ===== Capabilities + # Same as #fetch. + def uid_fetch(set, attr, mod = nil, changedsince: nil) + fetch_internal("UID FETCH", set, attr, mod, changedsince: changedsince) end # Sends a {STORE command [IMAP4rev1 §6.4.6]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.6] @@ -2760,7 +2784,11 @@ def search_internal(cmd, keys, charset) end end - def fetch_internal(cmd, set, attr, mod = nil) + def fetch_internal(cmd, set, attr, mod = nil, changedsince: nil) + if changedsince + mod ||= [] + mod << "CHANGEDSINCE" << Integer(changedsince) + end case attr when String then attr = RawData.new(attr) diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index 3ce5695b..5fdcb807 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -1151,6 +1151,24 @@ def test_clear_responses end end + test "#fetch with changedsince" do + with_fake_server select: "inbox" do |server, imap| + server.on("FETCH", &:done_ok) + imap.fetch 1..-1, %w[FLAGS], changedsince: 12345 + assert_equal("RUBY0002 FETCH 1:* (FLAGS) (CHANGEDSINCE 12345)", + server.commands.pop.raw.strip) + end + end + + test "#uid_fetch with changedsince" do + with_fake_server select: "inbox" do |server, imap| + server.on("UID FETCH", &:done_ok) + imap.uid_fetch 1..-1, %w[FLAGS], changedsince: 12345 + assert_equal("RUBY0002 UID FETCH 1:* (FLAGS) (CHANGEDSINCE 12345)", + server.commands.pop.raw.strip) + end + end + def test_close with_fake_server(select: "inbox") do |server, imap| resp = imap.close From f40e758a91926e1f0b89a9315f6d9c45520cb0e3 Mon Sep 17 00:00:00 2001 From: nicholas evans Date: Fri, 24 Nov 2023 15:44:01 -0500 Subject: [PATCH 7/8] =?UTF-8?q?=E2=9C=A8=20Add=20`unchangedsince`=20kwarg?= =?UTF-8?q?=20to=20`#store`,=20`#uid=5Fstore`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #237. --- lib/net/imap.rb | 66 +++++++++++++++++++++++++++++--------- test/net/imap/test_imap.rb | 22 +++++++++++++ 2 files changed, 72 insertions(+), 16 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 30451697..7f7da6d0 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -513,6 +513,8 @@ module Net # (but thread responses are unchanged). # - Updates #fetch and #uid_fetch with the +changedsince+ modifier and # +MODSEQ+ FetchData attribute. + # - Updates #store and #uid_store with the +unchangedsince+ modifier and adds + # the +MODIFIED+ ResponseCode to the tagged response. # # ==== RFC8438: STATUS=SIZE # - Updates #status with the +SIZE+ status attribute. @@ -2066,13 +2068,29 @@ def uid_fetch(set, attr, mod = nil, changedsince: nil) fetch_internal("UID FETCH", set, attr, mod, changedsince: changedsince) end + # :call-seq: + # store(set, attr, value, unchangedsince: nil) -> array of FetchData + # # Sends a {STORE command [IMAP4rev1 §6.4.6]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.6] # to alter data associated with messages in the mailbox, in particular their - # flags. The +set+ parameter is a number, an array of numbers, or a Range - # object. Each number is a message sequence number. +attr+ is the name of a - # data item to store: "FLAGS" will replace the message's flag list - # with the provided one, "+FLAGS" will add the provided flags, and - # "-FLAGS" will remove them. +flags+ is a list of flags. + # flags. + # + # +set+ is a number, an array of numbers, or a Range object. Each number is + # a message sequence number. + # + # +attr+ is the name of a data item to store. The semantics of +value+ + # varies based on +attr+: + # * When +attr+ is "FLAGS", the flags in +value+ replace the + # message's flag list. + # * When +attr+ is "+FLAGS", the flags in +value+ are added to + # the flags for the message. + # * When +attr+ is "-FLAGS", the flags in +value+ are removed + # from the message. + # + # +unchangedsince+ is an optional integer mod-sequence. It prohibits any + # changes to messages with +mod-sequence+ greater than the specified + # +unchangedsince+ value. A SequenceSet of any messages that fail this + # check will be returned in a +MODIFIED+ ResponseCode. # # The return value is an array of FetchData. # @@ -2081,13 +2099,25 @@ def uid_fetch(set, attr, mod = nil, changedsince: nil) # ===== For example: # # p imap.store(6..8, "+FLAGS", [:Deleted]) - # #=> [#[:Seen, :Deleted]}>, \\ - # #[:Seen, :Deleted]}>, \\ + # #=> [#[:Seen, :Deleted]}>, + # #[:Seen, :Deleted]}>, # #[:Seen, :Deleted]}>] - def store(set, attr, flags) - return store_internal("STORE", set, attr, flags) + # + # ===== Capabilities + # + # Extensions may define new data items to be used with #store. + # + # The server's capabilities must include +CONDSTORE+ + # {[RFC7162]}[https://tools.ietf.org/html/rfc7162] in order to use the + # +unchangedsince+ argument. Using +unchangedsince+ implicitly enables the + # +CONDSTORE+ extension. + def store(set, attr, flags, unchangedsince: nil) + store_internal("STORE", set, attr, flags, unchangedsince: unchangedsince) end + # :call-seq: + # uid_store(set, attr, value, unchangedsince: nil) -> array of FetchData + # # Sends a {UID STORE command [IMAP4rev1 §6.4.8]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.8] # to alter data associated with messages in the mailbox, in particular their # flags. @@ -2096,8 +2126,11 @@ def store(set, attr, flags) # message sequence numbers. # # Related: #store - def uid_store(set, attr, flags) - return store_internal("UID STORE", set, attr, flags) + # + # ===== Capabilities + # Same as #store. + def uid_store(set, attr, flags, unchangedsince: nil) + store_internal("UID STORE", set, attr, flags, unchangedsince: unchangedsince) end # Sends a {COPY command [IMAP4rev1 §6.4.7]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.7] @@ -2809,13 +2842,14 @@ def fetch_internal(cmd, set, attr, mod = nil, changedsince: nil) end end - def store_internal(cmd, set, attr, flags) - if attr.instance_of?(String) - attr = RawData.new(attr) - end + def store_internal(cmd, set, attr, flags, unchangedsince: nil) + attr = RawData.new(attr) if attr.instance_of?(String) + args = [MessageSet.new(set)] + args << ["UNCHANGEDSINCE", Integer(unchangedsince)] if unchangedsince + args << attr << flags synchronize do clear_responses("FETCH") - send_command(cmd, MessageSet.new(set), attr, flags) + send_command(cmd, *args) clear_responses("FETCH") end end diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index 5fdcb807..a08ec105 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -1169,6 +1169,28 @@ def test_clear_responses end end + test "#store with unchangedsince" do + with_fake_server select: "inbox" do |server, imap| + server.on("STORE", &:done_ok) + imap.store 1..-1, "FLAGS", %i[Deleted], unchangedsince: 12345 + assert_equal( + "RUBY0002 STORE 1:* (UNCHANGEDSINCE 12345) FLAGS (\\Deleted)", + server.commands.pop.raw.strip + ) + end + end + + test "#uid_store with changedsince" do + with_fake_server select: "inbox" do |server, imap| + server.on("UID STORE", &:done_ok) + imap.uid_store 1..-1, "FLAGS", %i[Deleted], unchangedsince: 987 + assert_equal( + "RUBY0002 UID STORE 1:* (UNCHANGEDSINCE 987) FLAGS (\\Deleted)", + server.commands.pop.raw.strip + ) + end + end + def test_close with_fake_server(select: "inbox") do |server, imap| resp = imap.close From dd181d39890c88e431ff95888f3fdd0617b141aa Mon Sep 17 00:00:00 2001 From: nicholas evans Date: Fri, 24 Nov 2023 15:41:01 -0500 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=93=9A=20Add=20CONDSTORE=20extension?= =?UTF-8?q?=20to=20class=20documention?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 7f7da6d0..f6bbf4f8 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -504,6 +504,8 @@ module Net # # ==== RFC7162: +CONDSTORE+ # + # - Updates #enable with +CONDSTORE+ parameter. +CONDSTORE+ will also be + # enabled by using any of the extension's command parameters, listed below. # - Updates #status with the +HIGHESTMODSEQ+ status attribute. # - Updates #select and #examine with the +condstore+ modifier, and adds # either a +HIGHESTMODSEQ+ or +NOMODSEQ+ ResponseCode to the responses. @@ -683,6 +685,16 @@ module Net # Resnick, P., Ed., Newman, C., Ed., and S. Shen, Ed., # "IMAP Support for UTF-8", RFC 6855, DOI 10.17487/RFC6855, March 2013, # . + # [CONDSTORE[https://tools.ietf.org/html/rfc7162]]:: + # [QRESYNC[https://tools.ietf.org/html/rfc7162]]:: + # Melnikov, A. and D. Cridland, "IMAP Extensions: Quick Flag Changes + # Resynchronization (CONDSTORE) and Quick Mailbox Resynchronization + # (QRESYNC)", RFC 7162, DOI 10.17487/RFC7162, May 2014, + # . + # [OBJECTID[https://tools.ietf.org/html/rfc8474]]:: + # Gondwana, B., Ed., "IMAP Extension for Object Identifiers", + # RFC 8474, DOI 10.17487/RFC8474, September 2018, + # . # # === IANA registries # * {IMAP Capabilities}[http://www.iana.org/assignments/imap4-capabilities] @@ -2306,6 +2318,13 @@ def uid_thread(algorithm, search_keys, charset) # each enabled extension (usually the same name as the enabled extension). # The following capabilities may be enabled: # + # [+CONDSTORE+ {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html]] + # + # Updates various commands to return +CONDSTORE+ extension responses. It + # is not necessary to explicitly enable +CONDSTORE+—using any of the + # command parameters defined by the extension will implicitly enable it. + # See {[RFC7162 §3.1]}[https://www.rfc-editor.org/rfc/rfc7162.html#section-3.1]. + # # [+:utf8+ --- an alias for "UTF8=ACCEPT"] # # In a future release, enable(:utf8) will enable either