diff --git a/lib/net/imap/config.rb b/lib/net/imap/config.rb index 452ef0aa..a8d546c6 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_data.rb b/lib/net/imap/response_data.rb index e2335186..8013b4f2 100644 --- a/lib/net/imap/response_data.rb +++ b/lib/net/imap/response_data.rb @@ -5,6 +5,9 @@ class IMAP < Protocol autoload :FetchData, "#{__dir__}/fetch_data" autoload :SearchResult, "#{__dir__}/search_result" 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. # @@ -324,60 +327,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/response_parser.rb b/lib/net/imap/response_parser.rb index f783810b..e9775c00 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 - UIDPlusData.new(validity, nil, dst_uids) + AppendUID(validity, dst_uids) end # already matched: "COPYUID" @@ -1881,6 +1880,17 @@ def resp_code_copy__data validity = number; SP! src_uids = uid_set; SP! dst_uids = uid_set + CopyUID(validity, src_uids, dst_uids) + end + + 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) end @@ -2007,15 +2017,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/lib/net/imap/uidplus_data.rb b/lib/net/imap/uidplus_data.rb new file mode 100644 index 00000000..679b0b2b --- /dev/null +++ b/lib/net/imap/uidplus_data.rb @@ -0,0 +1,326 @@ +# frozen_string_literal: true + +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]. + # + # 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]] + # 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 + + # 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 + # Config#parser_use_deprecated_uidplus_data to +false+. + # + # 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 + + # 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 + # Config#parser_use_deprecated_uidplus_data to +false+. + # + # 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_imap_response_data.rb b/test/net/imap/test_imap_response_data.rb deleted file mode 100644 index 0bee8b9a..00000000 --- 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 aef6f1da..e85b0b4e 100644 --- a/test/net/imap/test_imap_response_parser.rb +++ b/test/net/imap/test_imap_response_parser.rb @@ -193,4 +193,80 @@ 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 "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( + "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 + + 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 + + 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 diff --git a/test/net/imap/test_thread_member.rb b/test/net/imap/test_thread_member.rb new file mode 100644 index 00000000..afa933fb --- /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 diff --git a/test/net/imap/test_uidplus_data.rb b/test/net/imap/test_uidplus_data.rb new file mode 100644 index 00000000..0088c346 --- /dev/null +++ b/test/net/imap/test_uidplus_data.rb @@ -0,0 +1,232 @@ +# 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 + +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 + +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