Skip to content

Commit

Permalink
✨ Add CopyUIDData (to replace UIDPlusData)
Browse files Browse the repository at this point in the history
  • Loading branch information
nevans committed Feb 6, 2025
1 parent 01bb49f commit bcb261d
Show file tree
Hide file tree
Showing 3 changed files with 278 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/net/imap/response_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
127 changes: 127 additions & 0 deletions lib/net/imap/uidplus_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
#
# <em>*Warning:*</em> 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
150 changes: 150 additions & 0 deletions test/net/imap/test_uidplus_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit bcb261d

Please sign in to comment.