Skip to content

✨ Add AppendUIDData and CopyUIDData classes #400

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/net/imap/response_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ class IMAP < Protocol
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"
autoload :VanishedData, "#{__dir__}/vanished_data"

# Net::IMAP::ContinuationRequest represents command continuation requests.
Expand Down
166 changes: 166 additions & 0 deletions lib/net/imap/uidplus_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,171 @@ 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

# 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
186 changes: 186 additions & 0 deletions test/net/imap/test_uidplus_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,189 @@ 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

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