From 01bb49f4ae3220a695e21314ba4d92a84fe64b35 Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 5 Feb 2025 16:25:11 -0500 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20Add=20AppendUIDData=20(to=20rep?= =?UTF-8?q?lace=20UIDPlusData)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap/response_data.rb | 1 + lib/net/imap/uidplus_data.rb | 39 ++++++++++++++++++++++++++++++ test/net/imap/test_uidplus_data.rb | 36 +++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/lib/net/imap/response_data.rb b/lib/net/imap/response_data.rb index 40586ebd..37f06c22 100644 --- a/lib/net/imap/response_data.rb +++ b/lib/net/imap/response_data.rb @@ -8,6 +8,7 @@ class IMAP < Protocol autoload :SearchResult, "#{__dir__}/search_result" autoload :SequenceSet, "#{__dir__}/sequence_set" autoload :UIDPlusData, "#{__dir__}/uidplus_data" + autoload :AppendUIDData, "#{__dir__}/uidplus_data" autoload :VanishedData, "#{__dir__}/vanished_data" # Net::IMAP::ContinuationRequest represents command continuation requests. diff --git a/lib/net/imap/uidplus_data.rb b/lib/net/imap/uidplus_data.rb index 687e34c7..dae0bf01 100644 --- a/lib/net/imap/uidplus_data.rb +++ b/lib/net/imap/uidplus_data.rb @@ -60,5 +60,44 @@ def uid_mapping end end + # AppendUIDData represents the ResponseCode#data that accompanies the + # +APPENDUID+ {response code}[rdoc-ref:ResponseCode]. + # + # A server that supports +UIDPLUS+ (or +IMAP4rev2+) should send + # AppendUIDData inside every TaggedResponse returned by the + # append[rdoc-ref:Net::IMAP#append] command---unless the target mailbox + # reports +UIDNOTSTICKY+. + # + # == Required capability + # Requires either +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315]] + # or +IMAP4rev2+ capability. + class AppendUIDData < Data.define(:uidvalidity, :assigned_uids) + def initialize(uidvalidity:, assigned_uids:) + uidvalidity = Integer(uidvalidity) + assigned_uids = SequenceSet[assigned_uids] + NumValidator.ensure_nz_number(uidvalidity) + if assigned_uids.include_star? + raise DataFormatError, "uid-set cannot contain '*'" + end + super + end + + ## + # attr_reader: uidvalidity + # :call-seq: uidvalidity -> nonzero uint32 + # + # The UIDVALIDITY of the destination mailbox. + + ## + # attr_reader: assigned_uids + # + # A SequenceSet with the newly assigned UIDs of the appended messages. + + # Returns the number of messages that have been appended. + def size + assigned_uids.count_with_duplicates + end + end + end end diff --git a/test/net/imap/test_uidplus_data.rb b/test/net/imap/test_uidplus_data.rb index 210a000e..0d693ae9 100644 --- a/test/net/imap/test_uidplus_data.rb +++ b/test/net/imap/test_uidplus_data.rb @@ -44,3 +44,39 @@ class TestUIDPlusData < Test::Unit::TestCase end end + +class TestAppendUIDData < Test::Unit::TestCase + # alias for convenience + AppendUIDData = Net::IMAP::AppendUIDData + SequenceSet = Net::IMAP::SequenceSet + DataFormatError = Net::IMAP::DataFormatError + UINT32_MAX = 2**32 - 1 + + test "#uidvalidity must be valid nz-number" do + assert_equal 1, AppendUIDData.new(1, 99).uidvalidity + assert_equal UINT32_MAX, AppendUIDData.new(UINT32_MAX, 1).uidvalidity + assert_raise DataFormatError do AppendUIDData.new(0, 1) end + assert_raise DataFormatError do AppendUIDData.new(2**32, 1) end + end + + test "#assigned_uids must be a valid uid-set" do + assert_equal SequenceSet[1], AppendUIDData.new(99, "1").assigned_uids + assert_equal SequenceSet[1..9], AppendUIDData.new(1, "1:9").assigned_uids + assert_equal(SequenceSet[UINT32_MAX], + AppendUIDData.new(1, UINT32_MAX.to_s).assigned_uids) + assert_raise DataFormatError do AppendUIDData.new(1, 0) end + assert_raise DataFormatError do AppendUIDData.new(1, "*") end + assert_raise DataFormatError do AppendUIDData.new(1, "1:*") end + end + + test "#size returns the number of UIDs" do + assert_equal(10, AppendUIDData.new(1, "1:10").size) + assert_equal(4_000_000_000, AppendUIDData.new(1, 1..4_000_000_000).size) + end + + test "#assigned_uids is converted to SequenceSet" do + assert_equal SequenceSet[1], AppendUIDData.new(99, "1").assigned_uids + assert_equal SequenceSet[1..4], AppendUIDData.new(1, [1, 2, 3, 4]).assigned_uids + end + +end From bcb261d12e9911eaf89d35db314c626501c92b72 Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 5 Feb 2025 16:25:36 -0500 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8=20Add=20CopyUIDData=20(to=20repla?= =?UTF-8?q?ce=20UIDPlusData)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap/response_data.rb | 1 + lib/net/imap/uidplus_data.rb | 127 ++++++++++++++++++++++++ test/net/imap/test_uidplus_data.rb | 150 +++++++++++++++++++++++++++++ 3 files changed, 278 insertions(+) diff --git a/lib/net/imap/response_data.rb b/lib/net/imap/response_data.rb index 37f06c22..d862deaa 100644 --- a/lib/net/imap/response_data.rb +++ b/lib/net/imap/response_data.rb @@ -9,6 +9,7 @@ class IMAP < Protocol autoload :SequenceSet, "#{__dir__}/sequence_set" autoload :UIDPlusData, "#{__dir__}/uidplus_data" autoload :AppendUIDData, "#{__dir__}/uidplus_data" + autoload :CopyUIDData, "#{__dir__}/uidplus_data" autoload :VanishedData, "#{__dir__}/vanished_data" # Net::IMAP::ContinuationRequest represents command continuation requests. diff --git a/lib/net/imap/uidplus_data.rb b/lib/net/imap/uidplus_data.rb index dae0bf01..f937d53d 100644 --- a/lib/net/imap/uidplus_data.rb +++ b/lib/net/imap/uidplus_data.rb @@ -99,5 +99,132 @@ def size end end + # CopyUIDData represents the ResponseCode#data that accompanies the + # +COPYUID+ {response code}[rdoc-ref:ResponseCode]. + # + # A server that supports +UIDPLUS+ (or +IMAP4rev2+) should send CopyUIDData + # in response to + # copy[rdoc-ref:Net::IMAP#copy], {uid_copy}[rdoc-ref:Net::IMAP#uid_copy], + # move[rdoc-ref:Net::IMAP#copy], and {uid_move}[rdoc-ref:Net::IMAP#uid_move] + # commands---unless the destination mailbox reports +UIDNOTSTICKY+. + # + # Note that copy[rdoc-ref:Net::IMAP#copy] and + # {uid_copy}[rdoc-ref:Net::IMAP#uid_copy] return CopyUIDData in their + # TaggedResponse. But move[rdoc-ref:Net::IMAP#copy] and + # {uid_move}[rdoc-ref:Net::IMAP#uid_move] _should_ send CopyUIDData in an + # UntaggedResponse response before sending their TaggedResponse. However + # some servers do send CopyUIDData in the TaggedResponse for +MOVE+ + # commands---this complies with the older +UIDPLUS+ specification but is + # discouraged by the +MOVE+ extension and disallowed by +IMAP4rev2+. + # + # == Required capability + # Requires either +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315]] + # or +IMAP4rev2+ capability. + class CopyUIDData < Data.define(:uidvalidity, :source_uids, :assigned_uids) + def initialize(uidvalidity:, source_uids:, assigned_uids:) + uidvalidity = Integer(uidvalidity) + source_uids = SequenceSet[source_uids] + assigned_uids = SequenceSet[assigned_uids] + NumValidator.ensure_nz_number(uidvalidity) + if source_uids.include_star? || assigned_uids.include_star? + raise DataFormatError, "uid-set cannot contain '*'" + elsif source_uids.count_with_duplicates != assigned_uids.count_with_duplicates + raise DataFormatError, "mismatched uid-set sizes for %s and %s" % [ + source_uids, assigned_uids + ] + end + super + end + + ## + # attr_reader: uidvalidity + # + # The +UIDVALIDITY+ of the destination mailbox (a nonzero unsigned 32 bit + # integer). + + ## + # attr_reader: source_uids + # + # A SequenceSet with the original UIDs of the copied or moved messages. + + ## + # attr_reader: assigned_uids + # + # A SequenceSet with the newly assigned UIDs of the copied or moved + # messages. + + # Returns the number of messages that have been copied or moved. + # source_uids and the assigned_uids will both the same number of UIDs. + def size + assigned_uids.count_with_duplicates + end + + # :call-seq: + # assigned_uid_for(source_uid) -> uid + # self[source_uid] -> uid + # + # Returns the UID in the destination mailbox for the message that was + # copied from +source_uid+ in the source mailbox. + # + # This is the reverse of #source_uid_for. + # + # Related: source_uid_for, each_uid_pair, uid_mapping + def assigned_uid_for(source_uid) + idx = source_uids.find_ordered_index(source_uid) and + assigned_uids.ordered_at(idx) + end + alias :[] :assigned_uid_for + + # :call-seq: + # source_uid_for(assigned_uid) -> uid + # + # Returns the UID in the source mailbox for the message that was copied to + # +assigned_uid+ in the source mailbox. + # + # This is the reverse of #assigned_uid_for. + # + # Related: assigned_uid_for, each_uid_pair, uid_mapping + def source_uid_for(assigned_uid) + idx = assigned_uids.find_ordered_index(assigned_uid) and + source_uids.ordered_at(idx) + end + + # Yields a pair of UIDs for each copied message. The first is the + # message's UID in the source mailbox and the second is the UID in the + # destination mailbox. + # + # Returns an enumerator when no block is given. + # + # Please note the warning on uid_mapping before calling methods like + # +to_h+ or +to_a+ on the returned enumerator. + # + # Related: uid_mapping, assigned_uid_for, source_uid_for + def each_uid_pair + return enum_for(__method__) unless block_given? + source_uids.each_ordered_number.lazy + .zip(assigned_uids.each_ordered_number.lazy) do + |source_uid, assigned_uid| + yield source_uid, assigned_uid + end + end + alias each_pair each_uid_pair + alias each each_uid_pair + + # :call-seq: uid_mapping -> hash + # + # Returns a hash mapping each source UID to the newly assigned destination + # UID. + # + # *Warning:* The hash that is created may consume _much_ more + # memory than the data used to create it. When handling responses from an + # untrusted server, check #size before calling this method. + # + # Related: each_uid_pair, assigned_uid_for, source_uid_for + def uid_mapping + each_uid_pair.to_h + end + + end + end end diff --git a/test/net/imap/test_uidplus_data.rb b/test/net/imap/test_uidplus_data.rb index 0d693ae9..0088c346 100644 --- a/test/net/imap/test_uidplus_data.rb +++ b/test/net/imap/test_uidplus_data.rb @@ -80,3 +80,153 @@ class TestAppendUIDData < Test::Unit::TestCase end end + +class TestCopyUIDData < Test::Unit::TestCase + # alias for convenience + CopyUIDData = Net::IMAP::CopyUIDData + SequenceSet = Net::IMAP::SequenceSet + DataFormatError = Net::IMAP::DataFormatError + UINT32_MAX = 2**32 - 1 + + test "#uidvalidity must be valid nz-number" do + assert_equal 1, CopyUIDData.new(1, 99, 99).uidvalidity + assert_equal UINT32_MAX, CopyUIDData.new(UINT32_MAX, 1, 1).uidvalidity + assert_raise DataFormatError do CopyUIDData.new(0, 1, 1) end + assert_raise DataFormatError do CopyUIDData.new(2**32, 1, 1) end + end + + test "#source_uids must be valid uid-set" do + assert_equal SequenceSet[1], CopyUIDData.new(99, "1", 99).source_uids + assert_equal SequenceSet[5..8], CopyUIDData.new(1, 5..8, 1..4).source_uids + assert_equal(SequenceSet[UINT32_MAX], + CopyUIDData.new(1, UINT32_MAX.to_s, 1).source_uids) + assert_raise DataFormatError do CopyUIDData.new(99, nil, 99) end + assert_raise DataFormatError do CopyUIDData.new(1, 0, 1) end + assert_raise DataFormatError do CopyUIDData.new(1, "*", 1) end + end + + test "#assigned_uids must be a valid uid-set" do + assert_equal SequenceSet[1], CopyUIDData.new(99, 1, "1").assigned_uids + assert_equal SequenceSet[1..9], CopyUIDData.new(1, 1..9, "1:9").assigned_uids + assert_equal(SequenceSet[UINT32_MAX], + CopyUIDData.new(1, 1, UINT32_MAX.to_s).assigned_uids) + assert_raise DataFormatError do CopyUIDData.new(1, 1, 0) end + assert_raise DataFormatError do CopyUIDData.new(1, 1, "*") end + assert_raise DataFormatError do CopyUIDData.new(1, 1, "1:*") end + end + + test "#size returns the number of UIDs" do + assert_equal(10, CopyUIDData.new(1, "9,8,7,6,1:5,10", "1:10").size) + assert_equal(4_000_000_000, + CopyUIDData.new( + 1, "2000000000:4000000000,1:1999999999", 1..4_000_000_000 + ).size) + end + + test "#source_uids and #assigned_uids must be same size" do + assert_raise DataFormatError do CopyUIDData.new(1, 1..5, 1) end + assert_raise DataFormatError do CopyUIDData.new(1, 1, 1..5) end + end + + test "#source_uids is converted to SequenceSet" do + assert_equal SequenceSet[1], CopyUIDData.new(99, "1", 99).source_uids + assert_equal SequenceSet[5, 6, 7, 8], CopyUIDData.new(1, 5..8, 1..4).source_uids + end + + test "#assigned_uids is converted to SequenceSet" do + assert_equal SequenceSet[1], CopyUIDData.new(99, 1, "1").assigned_uids + assert_equal SequenceSet[1, 2, 3, 4], CopyUIDData.new(1, "1:4", 1..4).assigned_uids + end + + test "#uid_mapping maps source_uids to assigned_uids" do + uidplus = CopyUIDData.new(9999, "20:19,500:495", "92:97,101:100") + assert_equal( + { + 19 => 92, + 20 => 93, + 495 => 94, + 496 => 95, + 497 => 96, + 498 => 97, + 499 => 100, + 500 => 101, + }, + uidplus.uid_mapping + ) + end + + test "#uid_mapping for with source_uids in unsorted order" do + uidplus = CopyUIDData.new(1, "495:500,20:19", "92:97,101:100") + assert_equal( + { + 495 => 92, + 496 => 93, + 497 => 94, + 498 => 95, + 499 => 96, + 500 => 97, + 19 => 100, + 20 => 101, + }, + uidplus.uid_mapping + ) + end + + test "#assigned_uid_for(source_uid)" do + uidplus = CopyUIDData.new(1, "495:500,20:19", "92:97,101:100") + assert_equal 92, uidplus.assigned_uid_for(495) + assert_equal 93, uidplus.assigned_uid_for(496) + assert_equal 94, uidplus.assigned_uid_for(497) + assert_equal 95, uidplus.assigned_uid_for(498) + assert_equal 96, uidplus.assigned_uid_for(499) + assert_equal 97, uidplus.assigned_uid_for(500) + assert_equal 100, uidplus.assigned_uid_for( 19) + assert_equal 101, uidplus.assigned_uid_for( 20) + end + + test "#[](source_uid)" do + uidplus = CopyUIDData.new(1, "495:500,20:19", "92:97,101:100") + assert_equal 92, uidplus[495] + assert_equal 93, uidplus[496] + assert_equal 94, uidplus[497] + assert_equal 95, uidplus[498] + assert_equal 96, uidplus[499] + assert_equal 97, uidplus[500] + assert_equal 100, uidplus[ 19] + assert_equal 101, uidplus[ 20] + end + + test "#source_uid_for(assigned_uid)" do + uidplus = CopyUIDData.new(1, "495:500,20:19", "92:97,101:100") + assert_equal 495, uidplus.source_uid_for( 92) + assert_equal 496, uidplus.source_uid_for( 93) + assert_equal 497, uidplus.source_uid_for( 94) + assert_equal 498, uidplus.source_uid_for( 95) + assert_equal 499, uidplus.source_uid_for( 96) + assert_equal 500, uidplus.source_uid_for( 97) + assert_equal 19, uidplus.source_uid_for(100) + assert_equal 20, uidplus.source_uid_for(101) + end + + test "#each_uid_pair" do + uidplus = CopyUIDData.new(1, "495:500,20:19", "92:97,101:100") + expected = { + 495 => 92, + 496 => 93, + 497 => 94, + 498 => 95, + 499 => 96, + 500 => 97, + 19 => 100, + 20 => 101, + } + actual = {} + uidplus.each_uid_pair do |src, dst| actual[src] = dst end + assert_equal expected, actual + assert_equal expected, uidplus.each_uid_pair.to_h + assert_equal expected.to_a, uidplus.each_uid_pair.to_a + assert_equal expected, uidplus.each_pair.to_h + assert_equal expected, uidplus.each.to_h + end + +end