From 1082b22846cf6a8b863ba621ef8a311db1543de4 Mon Sep 17 00:00:00 2001 From: nick evans Date: Thu, 23 Jan 2025 11:04:12 -0500 Subject: [PATCH 01/10] =?UTF-8?q?=F0=9F=9A=9A=20Move=20"response=20data"?= =?UTF-8?q?=20tests=20to=20appropriate=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/net/imap/test_imap_response_data.rb | 58 ---------------------- test/net/imap/test_imap_response_parser.rb | 21 ++++++++ test/net/imap/test_thread_member.rb | 27 ++++++++++ 3 files changed, 48 insertions(+), 58 deletions(-) delete mode 100644 test/net/imap/test_imap_response_data.rb create mode 100644 test/net/imap/test_thread_member.rb diff --git a/test/net/imap/test_imap_response_data.rb b/test/net/imap/test_imap_response_data.rb deleted file mode 100644 index 0bee8b9a5..000000000 --- a/test/net/imap/test_imap_response_data.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -require "net/imap" -require "test/unit" - -class IMAPResponseDataTest < Test::Unit::TestCase - - def setup - Net::IMAP.config.reset - @do_not_reverse_lookup = Socket.do_not_reverse_lookup - Socket.do_not_reverse_lookup = true - end - - def teardown - Socket.do_not_reverse_lookup = @do_not_reverse_lookup - end - - def test_uidplus_copyuid__uid_mapping - parser = Net::IMAP::ResponseParser.new - response = parser.parse( - "A004 OK [copyUID 9999 20:19,500:495 92:97,101:100] Done\r\n" - ) - code = response.data.code - assert_equal( - { - 19 => 92, - 20 => 93, - 495 => 94, - 496 => 95, - 497 => 96, - 498 => 97, - 499 => 100, - 500 => 101, - }, - code.data.uid_mapping - ) - end - - def test_thread_member_to_sequence_set - # copied from the fourth example in RFC5256: (3 6 (4 23)(44 7 96)) - thmember = Net::IMAP::ThreadMember.method :new - thread = thmember.(3, [ - thmember.(6, [ - thmember.(4, [ - thmember.(23, []) - ]), - thmember.(44, [ - thmember.(7, [ - thmember.(96, []) - ]) - ]) - ]) - ]) - expected = Net::IMAP::SequenceSet.new("3:4,6:7,23,44,96") - assert_equal(expected, thread.to_sequence_set) - end - -end diff --git a/test/net/imap/test_imap_response_parser.rb b/test/net/imap/test_imap_response_parser.rb index aef6f1da3..0a191468b 100644 --- a/test/net/imap/test_imap_response_parser.rb +++ b/test/net/imap/test_imap_response_parser.rb @@ -193,4 +193,25 @@ def test_fetch_binary_and_binary_size Net::IMAP.debug = debug end + test "COPYUID with backwards ranges" do + parser = Net::IMAP::ResponseParser.new + response = parser.parse( + "A004 OK [copyUID 9999 20:19,500:495 92:97,101:100] Done\r\n" + ) + code = response.data.code + assert_equal( + { + 19 => 92, + 20 => 93, + 495 => 94, + 496 => 95, + 497 => 96, + 498 => 97, + 499 => 100, + 500 => 101, + }, + code.data.uid_mapping + ) + end + end diff --git a/test/net/imap/test_thread_member.rb b/test/net/imap/test_thread_member.rb new file mode 100644 index 000000000..afa933fb5 --- /dev/null +++ b/test/net/imap/test_thread_member.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "net/imap" +require "test/unit" + +class ThreadMemberTest < Test::Unit::TestCase + + test "#to_sequence_set" do + # copied from the fourth example in RFC5256: (3 6 (4 23)(44 7 96)) + thmember = Net::IMAP::ThreadMember.method :new + thread = thmember.(3, [ + thmember.(6, [ + thmember.(4, [ + thmember.(23, []) + ]), + thmember.(44, [ + thmember.(7, [ + thmember.(96, []) + ]) + ]) + ]) + ]) + expected = Net::IMAP::SequenceSet.new("3:4,6:7,23,44,96") + assert_equal(expected, thread.to_sequence_set) + end + +end From e734f902b52f1716b7c539998642b82aff75853c Mon Sep 17 00:00:00 2001 From: nick evans Date: Thu, 23 Jan 2025 10:56:17 -0500 Subject: [PATCH 02/10] =?UTF-8?q?=E2=9C=85=20Improve=20UIDPlusData#uid=5Fm?= =?UTF-8?q?apping=20test=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Explicitly test how unsorted uid-sets are handled. --- test/net/imap/test_uid_plus_data.rb | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 test/net/imap/test_uid_plus_data.rb diff --git a/test/net/imap/test_uid_plus_data.rb b/test/net/imap/test_uid_plus_data.rb new file mode 100644 index 000000000..210a000e3 --- /dev/null +++ b/test/net/imap/test_uid_plus_data.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require "net/imap" +require "test/unit" + +class TestUIDPlusData < Test::Unit::TestCase + + test "#uid_mapping with sorted source_uids" do + uidplus = Net::IMAP::UIDPlusData.new( + 1, [19, 20, *(495..500)], [*(92..97), 100, 101], + ) + 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 = Net::IMAP::UIDPlusData.new( + 1, [*(495..500), 19, 20], [*(92..97), 100, 101], + ) + assert_equal( + { + 495 => 92, + 496 => 93, + 497 => 94, + 498 => 95, + 499 => 96, + 500 => 97, + 19 => 100, + 20 => 101, + }, + uidplus.uid_mapping + ) + end + +end From 66e99709c128fb1c998ec873935ca05e1d399fe9 Mon Sep 17 00:00:00 2001 From: nick evans Date: Sat, 18 Jan 2025 11:49:11 -0500 Subject: [PATCH 03/10] =?UTF-8?q?=F0=9F=9A=9A=20Move=20UIDPlusData=20to=20?= =?UTF-8?q?new=20file=20(with=20doc=20updates)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The class rdoc was updated to match v0.5, in order to simplify other backported commits. --- lib/net/imap/response_data.rb | 55 +--------------------------------- lib/net/imap/uidplus_data.rb | 56 +++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 54 deletions(-) create mode 100644 lib/net/imap/uidplus_data.rb diff --git a/lib/net/imap/response_data.rb b/lib/net/imap/response_data.rb index e2335186a..c464bbcdd 100644 --- a/lib/net/imap/response_data.rb +++ b/lib/net/imap/response_data.rb @@ -5,6 +5,7 @@ class IMAP < Protocol autoload :FetchData, "#{__dir__}/fetch_data" autoload :SearchResult, "#{__dir__}/search_result" autoload :SequenceSet, "#{__dir__}/sequence_set" + autoload :UIDPlusData, "#{__dir__}/uidplus_data" # Net::IMAP::ContinuationRequest represents command continuation requests. # @@ -324,60 +325,6 @@ class ResponseCode < Struct.new(:name, :data) # code data can take. end - # Net::IMAP::UIDPlusData represents the ResponseCode#data that accompanies - # the +APPENDUID+ and +COPYUID+ response codes. - # - # See [[UIDPLUS[https://www.rfc-editor.org/rfc/rfc4315.html]]. - # - # ==== Capability requirement - # - # The +UIDPLUS+ capability[rdoc-ref:Net::IMAP#capability] must be supported. - # A server that supports +UIDPLUS+ should send a UIDPlusData object inside - # every TaggedResponse returned by the append[rdoc-ref:Net::IMAP#append], - # copy[rdoc-ref:Net::IMAP#copy], move[rdoc-ref:Net::IMAP#move], {uid - # copy}[rdoc-ref:Net::IMAP#uid_copy], and {uid - # move}[rdoc-ref:Net::IMAP#uid_move] commands---unless the destination - # mailbox reports +UIDNOTSTICKY+. - # - #-- - # TODO: support MULTIAPPEND - #++ - # - class UIDPlusData < Struct.new(:uidvalidity, :source_uids, :assigned_uids) - ## - # method: uidvalidity - # :call-seq: uidvalidity -> nonzero uint32 - # - # The UIDVALIDITY of the destination mailbox. - - ## - # method: source_uids - # :call-seq: source_uids -> nil or an array of nonzero uint32 - # - # The UIDs of the copied or moved messages. - # - # Note:: Returns +nil+ for Net::IMAP#append. - - ## - # method: assigned_uids - # :call-seq: assigned_uids -> an array of nonzero uint32 - # - # The newly assigned UIDs of the copied, moved, or appended messages. - # - # Note:: This always returns an array, even when it contains only one UID. - - ## - # :call-seq: uid_mapping -> nil or a hash - # - # Returns a hash mapping each source UID to the newly assigned destination - # UID. - # - # Note:: Returns +nil+ for Net::IMAP#append. - def uid_mapping - source_uids&.zip(assigned_uids)&.to_h - end - end - # Net::IMAP::MailboxList represents contents of the LIST response, # representing a single mailbox path. # diff --git a/lib/net/imap/uidplus_data.rb b/lib/net/imap/uidplus_data.rb new file mode 100644 index 000000000..2c478e2c9 --- /dev/null +++ b/lib/net/imap/uidplus_data.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Net + class IMAP < Protocol + + # UIDPlusData represents the ResponseCode#data that accompanies the + # +APPENDUID+ and +COPYUID+ {response codes}[rdoc-ref:ResponseCode]. + # + # A server that supports +UIDPLUS+ should send a UIDPlusData object inside + # every TaggedResponse returned by the append[rdoc-ref:Net::IMAP#append], + # copy[rdoc-ref:Net::IMAP#copy], move[rdoc-ref:Net::IMAP#move], {uid + # copy}[rdoc-ref:Net::IMAP#uid_copy], and {uid + # move}[rdoc-ref:Net::IMAP#uid_move] commands---unless the destination + # mailbox reports +UIDNOTSTICKY+. + # + # == Required capability + # Requires either +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315]] + # or +IMAP4rev2+ capability. + # + class UIDPlusData < Struct.new(:uidvalidity, :source_uids, :assigned_uids) + ## + # method: uidvalidity + # :call-seq: uidvalidity -> nonzero uint32 + # + # The UIDVALIDITY of the destination mailbox. + + ## + # method: source_uids + # :call-seq: source_uids -> nil or an array of nonzero uint32 + # + # The UIDs of the copied or moved messages. + # + # Note:: Returns +nil+ for Net::IMAP#append. + + ## + # method: assigned_uids + # :call-seq: assigned_uids -> an array of nonzero uint32 + # + # The newly assigned UIDs of the copied, moved, or appended messages. + # + # Note:: This always returns an array, even when it contains only one UID. + + ## + # :call-seq: uid_mapping -> nil or a hash + # + # Returns a hash mapping each source UID to the newly assigned destination + # UID. + # + # Note:: Returns +nil+ for Net::IMAP#append. + def uid_mapping + source_uids&.zip(assigned_uids)&.to_h + end + end + + end +end From 12e166e05db7ea6bf40a6197168dc81a4f52ec48 Mon Sep 17 00:00:00 2001 From: nick evans Date: Sun, 19 Jan 2025 10:15:14 -0500 Subject: [PATCH 04/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Parse=20`uid-set`=20?= =?UTF-8?q?as=20`sequence-set`=20without=20`*`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In addition to letting us delete some code, this is also a step towards replacing `UIDPlusData` with new (incompatible) data structures that store UID sets directly, rather than converted into arrays of integers. --- lib/net/imap/response_parser.rb | 20 ++++++++++---------- test/net/imap/test_imap_response_parser.rb | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/lib/net/imap/response_parser.rb b/lib/net/imap/response_parser.rb index f783810bc..4fdb237d2 100644 --- a/lib/net/imap/response_parser.rb +++ b/lib/net/imap/response_parser.rb @@ -1871,7 +1871,7 @@ def charset__list def resp_code_apnd__data validity = number; SP! dst_uids = uid_set # uniqueid ⊂ uid-set - UIDPlusData.new(validity, nil, dst_uids) + UIDPlus(validity, nil, dst_uids) end # already matched: "COPYUID" @@ -1881,6 +1881,12 @@ def resp_code_copy__data validity = number; SP! src_uids = uid_set; SP! dst_uids = uid_set + UIDPlus(validity, src_uids, dst_uids) + end + + def UIDPlus(validity, src_uids, dst_uids) + src_uids &&= src_uids.each_ordered_number.to_a + dst_uids = dst_uids.each_ordered_number.to_a UIDPlusData.new(validity, src_uids, dst_uids) end @@ -2007,15 +2013,9 @@ def nparens__objectid; NIL? ? nil : parens__objectid end # uniqueid = nz-number # ; Strictly ascending def uid_set - token = match(T_NUMBER, T_ATOM) - case token.symbol - when T_NUMBER then [Integer(token.value)] - when T_ATOM - token.value.split(",").flat_map {|range| - range = range.split(":").map {|uniqueid| Integer(uniqueid) } - range.size == 1 ? range : Range.new(range.min, range.max).to_a - } - end + set = sequence_set + parse_error("uid-set cannot contain '*'") if set.include_star? + set end def nil_atom diff --git a/test/net/imap/test_imap_response_parser.rb b/test/net/imap/test_imap_response_parser.rb index 0a191468b..5073d574a 100644 --- a/test/net/imap/test_imap_response_parser.rb +++ b/test/net/imap/test_imap_response_parser.rb @@ -193,6 +193,15 @@ def test_fetch_binary_and_binary_size Net::IMAP.debug = debug end + test "APPENDUID with '*'" do + parser = Net::IMAP::ResponseParser.new + assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set cannot contain '\*'/ do + parser.parse( + "A004 OK [appendUID 1 1:*] Done\r\n" + ) + end + end + test "COPYUID with backwards ranges" do parser = Net::IMAP::ResponseParser.new response = parser.parse( @@ -214,4 +223,13 @@ def test_fetch_binary_and_binary_size ) end + test "COPYUID with '*'" do + parser = Net::IMAP::ResponseParser.new + assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set cannot contain '\*'/ do + parser.parse( + "A004 OK [copyUID 1 1:* 1:*] Done\r\n" + ) + end + end + end From 5310fb9d702c3d7068ff44cf4091661e4447347c Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 5 Feb 2025 16:31:36 -0500 Subject: [PATCH 05/10] =?UTF-8?q?=F0=9F=93=9A=20Document=20COPYUID=20in=20?= =?UTF-8?q?tagged=20vs=20untagged=20responses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap/uidplus_data.rb | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/net/imap/uidplus_data.rb b/lib/net/imap/uidplus_data.rb index 2c478e2c9..687e34c72 100644 --- a/lib/net/imap/uidplus_data.rb +++ b/lib/net/imap/uidplus_data.rb @@ -6,12 +6,20 @@ class IMAP < Protocol # UIDPlusData represents the ResponseCode#data that accompanies the # +APPENDUID+ and +COPYUID+ {response codes}[rdoc-ref:ResponseCode]. # - # A server that supports +UIDPLUS+ should send a UIDPlusData object inside - # every TaggedResponse returned by the append[rdoc-ref:Net::IMAP#append], - # copy[rdoc-ref:Net::IMAP#copy], move[rdoc-ref:Net::IMAP#move], {uid - # copy}[rdoc-ref:Net::IMAP#uid_copy], and {uid - # move}[rdoc-ref:Net::IMAP#uid_move] commands---unless the destination - # mailbox reports +UIDNOTSTICKY+. + # A server that supports +UIDPLUS+ should send UIDPlusData in response to + # the append[rdoc-ref:Net::IMAP#append], copy[rdoc-ref:Net::IMAP#copy], + # move[rdoc-ref:Net::IMAP#move], {uid copy}[rdoc-ref:Net::IMAP#uid_copy], + # and {uid move}[rdoc-ref:Net::IMAP#uid_move] commands---unless the + # destination mailbox reports +UIDNOTSTICKY+. + # + # Note that append[rdoc-ref:Net::IMAP#append], copy[rdoc-ref:Net::IMAP#copy] + # and {uid_copy}[rdoc-ref:Net::IMAP#uid_copy] return UIDPlusData in their + # TaggedResponse. But move[rdoc-ref:Net::IMAP#copy] and + # {uid_move}[rdoc-ref:Net::IMAP#uid_move] _should_ send UIDPlusData in an + # UntaggedResponse response before sending their TaggedResponse. However + # some servers do send UIDPlusData 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]] From f1e7433764d6bd685b1628b595ca0c8bf37419e1 Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 5 Feb 2025 16:24:54 -0500 Subject: [PATCH 06/10] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20UIDPLUS=20test=20?= =?UTF-8?q?file=20for=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/net/imap/{test_uid_plus_data.rb => test_uidplus_data.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/net/imap/{test_uid_plus_data.rb => test_uidplus_data.rb} (100%) diff --git a/test/net/imap/test_uid_plus_data.rb b/test/net/imap/test_uidplus_data.rb similarity index 100% rename from test/net/imap/test_uid_plus_data.rb rename to test/net/imap/test_uidplus_data.rb From 4c601c3a8468104eba5f9ee474f753064bf62115 Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 5 Feb 2025 16:25:11 -0500 Subject: [PATCH 07/10] =?UTF-8?q?=E2=9C=A8=20Add=20AppendUIDData=20(to=20r?= =?UTF-8?q?eplace=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 c464bbcdd..9fc5a5562 100644 --- a/lib/net/imap/response_data.rb +++ b/lib/net/imap/response_data.rb @@ -6,6 +6,7 @@ class IMAP < Protocol autoload :SearchResult, "#{__dir__}/search_result" autoload :SequenceSet, "#{__dir__}/sequence_set" autoload :UIDPlusData, "#{__dir__}/uidplus_data" + autoload :AppendUIDData, "#{__dir__}/uidplus_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 687e34c72..dae0bf011 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 210a000e3..0d693ae92 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 7e58ef35fae7b52eb24b39c87f5b7fc69fa3e757 Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 5 Feb 2025 16:25:36 -0500 Subject: [PATCH 08/10] =?UTF-8?q?=E2=9C=A8=20Add=20CopyUIDData=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 | 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 9fc5a5562..8013b4f21 100644 --- a/lib/net/imap/response_data.rb +++ b/lib/net/imap/response_data.rb @@ -7,6 +7,7 @@ class IMAP < Protocol autoload :SequenceSet, "#{__dir__}/sequence_set" autoload :UIDPlusData, "#{__dir__}/uidplus_data" autoload :AppendUIDData, "#{__dir__}/uidplus_data" + autoload :CopyUIDData, "#{__dir__}/uidplus_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 dae0bf011..f937d53df 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 0d693ae92..0088c3468 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 From 3c592fc98c26f11043a2b8e112bc91f31d7efad0 Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 5 Feb 2025 16:25:52 -0500 Subject: [PATCH 09/10] =?UTF-8?q?=F0=9F=94=A7=F0=9F=97=91=EF=B8=8F=20Depre?= =?UTF-8?q?cate=20UIDPlusData,=20with=20config=20to=20upgrade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This config attribute causes the parser to use the new AppendUIDData and CopyUIDData classes instead of CopyUIDData. AppendUIDData and CopyUIDData are _mostly_ backward-compatible with UIDPlusData. Most applications should be able to upgrade with no changes. UIDPlusData will be removed in +v0.6+. --- lib/net/imap/config.rb | 29 +++++++++++++++++ lib/net/imap/response_parser.rb | 12 ++++--- lib/net/imap/uidplus_data.rb | 14 ++++++++ test/net/imap/test_imap_response_parser.rb | 37 ++++++++++++++++++++++ 4 files changed, 88 insertions(+), 4 deletions(-) diff --git a/lib/net/imap/config.rb b/lib/net/imap/config.rb index 452ef0aaa..a8d546c6b 100644 --- a/lib/net/imap/config.rb +++ b/lib/net/imap/config.rb @@ -262,6 +262,32 @@ def self.[](config) # # Alias for responses_without_block + # Whether ResponseParser should use the deprecated UIDPlusData or + # CopyUIDData for +COPYUID+ response codes, and UIDPlusData or + # AppendUIDData for +APPENDUID+ response codes. + # + # AppendUIDData and CopyUIDData are _mostly_ backward-compatible with + # UIDPlusData. Most applications should be able to upgrade with little + # or no changes. + # + # (Parser support for +UIDPLUS+ added in +v0.3.2+.) + # + # (Config option added in +v0.4.19+ and +v0.5.6+.) + # + # UIDPlusData will be removed in +v0.6+ and this config setting will + # be ignored. + # + # ==== Valid options + # + # [+true+ (original default)] + # ResponseParser only uses UIDPlusData. + # + # [+false+ (planned default for +v0.6+)] + # ResponseParser _only_ uses AppendUIDData and CopyUIDData. + attr_accessor :parser_use_deprecated_uidplus_data, type: [ + true, false + ] + # Creates a new config object and initialize its attribute with +attrs+. # # If +parent+ is not given, the global config is used by default. @@ -341,6 +367,7 @@ def defaults_hash idle_response_timeout: 5, sasl_ir: true, responses_without_block: :silence_deprecation_warning, + parser_use_deprecated_uidplus_data: true, ).freeze @global = default.new @@ -349,6 +376,7 @@ def defaults_hash version_defaults[0] = Config[0.4].dup.update( sasl_ir: false, + parser_use_deprecated_uidplus_data: true, ).freeze version_defaults[0.0] = Config[0] version_defaults[0.1] = Config[0] @@ -365,6 +393,7 @@ def defaults_hash version_defaults[0.6] = Config[0.5].dup.update( responses_without_block: :frozen_dup, + parser_use_deprecated_uidplus_data: false, ).freeze version_defaults[:future] = Config[0.6] diff --git a/lib/net/imap/response_parser.rb b/lib/net/imap/response_parser.rb index 4fdb237d2..e9775c005 100644 --- a/lib/net/imap/response_parser.rb +++ b/lib/net/imap/response_parser.rb @@ -1867,11 +1867,10 @@ def charset__list # # n.b, uniqueid ⊂ uid-set. To avoid inconsistent return types, we always # match uid_set even if that returns a single-member array. - # def resp_code_apnd__data validity = number; SP! dst_uids = uid_set # uniqueid ⊂ uid-set - UIDPlus(validity, nil, dst_uids) + AppendUID(validity, dst_uids) end # already matched: "COPYUID" @@ -1881,10 +1880,15 @@ def resp_code_copy__data validity = number; SP! src_uids = uid_set; SP! dst_uids = uid_set - UIDPlus(validity, src_uids, dst_uids) + CopyUID(validity, src_uids, dst_uids) end - def UIDPlus(validity, src_uids, dst_uids) + def AppendUID(...) DeprecatedUIDPlus(...) || AppendUIDData.new(...) end + def CopyUID(...) DeprecatedUIDPlus(...) || CopyUIDData.new(...) end + + # TODO: remove this code in the v0.6.0 release + def DeprecatedUIDPlus(validity, src_uids = nil, dst_uids) + return unless config.parser_use_deprecated_uidplus_data src_uids &&= src_uids.each_ordered_number.to_a dst_uids = dst_uids.each_ordered_number.to_a UIDPlusData.new(validity, src_uids, dst_uids) diff --git a/lib/net/imap/uidplus_data.rb b/lib/net/imap/uidplus_data.rb index f937d53df..0e5936366 100644 --- a/lib/net/imap/uidplus_data.rb +++ b/lib/net/imap/uidplus_data.rb @@ -3,6 +3,10 @@ module Net class IMAP < Protocol + # *NOTE:* UIDPlusData is deprecated and will be removed in the +0.6.0+ + # release. To use AppendUIDData and CopyUIDData before +0.6.0+, set + # Config#parser_use_deprecated_uidplus_data to +false+. + # # UIDPlusData represents the ResponseCode#data that accompanies the # +APPENDUID+ and +COPYUID+ {response codes}[rdoc-ref:ResponseCode]. # @@ -60,6 +64,11 @@ def uid_mapping end end + # >>> + # *NOTE:* AppendUIDData will replace UIDPlusData for +APPENDUID+ in the + # +0.6.0+ release. To use AppendUIDData before +0.6.0+, set + # Config#parser_use_deprecated_uidplus_data to +false+. + # # AppendUIDData represents the ResponseCode#data that accompanies the # +APPENDUID+ {response code}[rdoc-ref:ResponseCode]. # @@ -99,6 +108,11 @@ def size end end + # >>> + # *NOTE:* CopyUIDData will replace UIDPlusData for +COPYUID+ in the + # +0.6.0+ release. To use CopyUIDData before +0.6.0+, set + # Config#parser_use_deprecated_uidplus_data to +false+. + # # CopyUIDData represents the ResponseCode#data that accompanies the # +COPYUID+ {response code}[rdoc-ref:ResponseCode]. # diff --git a/test/net/imap/test_imap_response_parser.rb b/test/net/imap/test_imap_response_parser.rb index 5073d574a..e85b0b4ec 100644 --- a/test/net/imap/test_imap_response_parser.rb +++ b/test/net/imap/test_imap_response_parser.rb @@ -202,6 +202,24 @@ def test_fetch_binary_and_binary_size end end + test "APPENDUID with parser_use_deprecated_uidplus_data = true" do + parser = Net::IMAP::ResponseParser.new(config: { + parser_use_deprecated_uidplus_data: true, + }) + response = parser.parse("A004 OK [APPENDUID 1 101:200] Done\r\n") + uidplus = response.data.code.data + assert_instance_of Net::IMAP::UIDPlusData, uidplus + assert_equal 100, uidplus.assigned_uids.size + end + + test "APPENDUID with parser_use_deprecated_uidplus_data = false" do + parser = Net::IMAP::ResponseParser.new(config: { + parser_use_deprecated_uidplus_data: false, + }) + response = parser.parse("A004 OK [APPENDUID 1 10] Done\r\n") + assert_instance_of Net::IMAP::AppendUIDData, response.data.code.data + end + test "COPYUID with backwards ranges" do parser = Net::IMAP::ResponseParser.new response = parser.parse( @@ -232,4 +250,23 @@ def test_fetch_binary_and_binary_size end end + test "COPYUID with parser_use_deprecated_uidplus_data = true" do + parser = Net::IMAP::ResponseParser.new(config: { + parser_use_deprecated_uidplus_data: true, + }) + response = parser.parse("A004 OK [copyUID 1 101:200 1:100] Done\r\n") + uidplus = response.data.code.data + assert_instance_of Net::IMAP::UIDPlusData, uidplus + assert_equal 100, uidplus.assigned_uids.size + assert_equal 100, uidplus.source_uids.size + end + + test "COPYUID with parser_use_deprecated_uidplus_data = false" do + parser = Net::IMAP::ResponseParser.new(config: { + parser_use_deprecated_uidplus_data: false, + }) + response = parser.parse("A004 OK [COPYUID 1 101 1] Done\r\n") + assert_instance_of Net::IMAP::CopyUIDData, response.data.code.data + end + end From d32320a74980a8754c5da3389693af8f597ab39c Mon Sep 17 00:00:00 2001 From: nick evans Date: Fri, 7 Feb 2025 12:53:42 -0500 Subject: [PATCH 10/10] =?UTF-8?q?=F0=9F=90=9B=20Fix=20missing=20`Data.defi?= =?UTF-8?q?ne`=20for=20new=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since its only these two classes, I manually wrote a subset of the code that would be generated by `Data.define` and inherited from `Data`. The `AppendUIDData` and `CopyUIDData` class definitions are unchanged—only the superclass definition is different. --- lib/net/imap/uidplus_data.rb | 86 +++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/lib/net/imap/uidplus_data.rb b/lib/net/imap/uidplus_data.rb index 0e5936366..679b0b2ba 100644 --- a/lib/net/imap/uidplus_data.rb +++ b/lib/net/imap/uidplus_data.rb @@ -64,6 +64,44 @@ def uid_mapping end end + # This replaces the `Data.define` polyfill that's used by net-imap 0.5. + class Data_define__uidvalidity___assigned_uids_ # :no-doc: + attr_reader :uidvalidity, :assigned_uids + + def self.[](...) new(...) end + def self.new(uidvalidity = (args = false; nil), + assigned_uids = nil, + **kwargs) + if kwargs.empty? + super(uidvalidity: uidvalidity, assigned_uids: assigned_uids) + elsif !args + super + else + raise ArgumentError, "sent both positional and keyword args" + end + end + + def ==(other) + self.class == other.class && + self.uidvalidity == other.uidvalidity && + self.assigned_uids == other.assigned_uids + end + + def eql?(other) + self.class.eql?(other.class) && + self.uidvalidity.eql?(other.uidvalidity) && + self.assigned_uids.eql?(other.assigned_uids) + end + + def hash; [self.class, uidvalidity, assigned_uids].hash end + + def initialize(uidvalidity:, assigned_uids:) + @uidvalidity = uidvalidity + @assigned_uids = assigned_uids + freeze + end + end + # >>> # *NOTE:* AppendUIDData will replace UIDPlusData for +APPENDUID+ in the # +0.6.0+ release. To use AppendUIDData before +0.6.0+, set @@ -80,7 +118,7 @@ def uid_mapping # == Required capability # Requires either +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315]] # or +IMAP4rev2+ capability. - class AppendUIDData < Data.define(:uidvalidity, :assigned_uids) + class AppendUIDData < Data_define__uidvalidity___assigned_uids_ def initialize(uidvalidity:, assigned_uids:) uidvalidity = Integer(uidvalidity) assigned_uids = SequenceSet[assigned_uids] @@ -108,6 +146,50 @@ def size end end + # This replaces the `Data.define` polyfill that's used by net-imap 0.5. + class Data_define__uidvalidity___source_uids___assigned_uids_ # :no-doc: + attr_reader :uidvalidity, :source_uids, :assigned_uids + + def self.[](...) new(...) end + def self.new(uidvalidity = (args = false; nil), + source_uids = nil, + assigned_uids = nil, + **kwargs) + if kwargs.empty? + super(uidvalidity: uidvalidity, + source_uids: source_uids, + assigned_uids: assigned_uids) + elsif !args + super(**kwargs) + else + raise ArgumentError, "sent both positional and keyword args" + end + end + + def initialize(uidvalidity:, source_uids:, assigned_uids:) + @uidvalidity = uidvalidity + @source_uids = source_uids + @assigned_uids = assigned_uids + freeze + end + + def ==(other) + self.class == other.class && + self.uidvalidity == other.uidvalidity && + self.source_uids == other.source_uids + self.assigned_uids == other.assigned_uids + end + + def eql?(other) + self.class.eql?(other.class) && + self.uidvalidity.eql?(other.uidvalidity) && + self.source_uids.eql?(other.source_uids) + self.assigned_uids.eql?(other.assigned_uids) + end + + def hash; [self.class, uidvalidity, source_uids, assigned_uids].hash end + end + # >>> # *NOTE:* CopyUIDData will replace UIDPlusData for +COPYUID+ in the # +0.6.0+ release. To use CopyUIDData before +0.6.0+, set @@ -134,7 +216,7 @@ def size # == 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) + class CopyUIDData < Data_define__uidvalidity___source_uids___assigned_uids_ def initialize(uidvalidity:, source_uids:, assigned_uids:) uidvalidity = Integer(uidvalidity) source_uids = SequenceSet[source_uids]