Skip to content

Commit 8f41dea

Browse files
authored
🔀 Merge pull request #400 from ruby/add-appenduid-copyuid-classes
✨ Add AppendUIDData and CopyUIDData classes
2 parents 85d0aa2 + bcb261d commit 8f41dea

File tree

3 files changed

+354
-0
lines changed

3 files changed

+354
-0
lines changed

‎lib/net/imap/response_data.rb

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ class IMAP < Protocol
88
autoload :SearchResult, "#{__dir__}/search_result"
99
autoload :SequenceSet, "#{__dir__}/sequence_set"
1010
autoload :UIDPlusData, "#{__dir__}/uidplus_data"
11+
autoload :AppendUIDData, "#{__dir__}/uidplus_data"
12+
autoload :CopyUIDData, "#{__dir__}/uidplus_data"
1113
autoload :VanishedData, "#{__dir__}/vanished_data"
1214

1315
# Net::IMAP::ContinuationRequest represents command continuation requests.

‎lib/net/imap/uidplus_data.rb

+166
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,171 @@ def uid_mapping
6060
end
6161
end
6262

63+
# AppendUIDData represents the ResponseCode#data that accompanies the
64+
# +APPENDUID+ {response code}[rdoc-ref:ResponseCode].
65+
#
66+
# A server that supports +UIDPLUS+ (or +IMAP4rev2+) should send
67+
# AppendUIDData inside every TaggedResponse returned by the
68+
# append[rdoc-ref:Net::IMAP#append] command---unless the target mailbox
69+
# reports +UIDNOTSTICKY+.
70+
#
71+
# == Required capability
72+
# Requires either +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315]]
73+
# or +IMAP4rev2+ capability.
74+
class AppendUIDData < Data.define(:uidvalidity, :assigned_uids)
75+
def initialize(uidvalidity:, assigned_uids:)
76+
uidvalidity = Integer(uidvalidity)
77+
assigned_uids = SequenceSet[assigned_uids]
78+
NumValidator.ensure_nz_number(uidvalidity)
79+
if assigned_uids.include_star?
80+
raise DataFormatError, "uid-set cannot contain '*'"
81+
end
82+
super
83+
end
84+
85+
##
86+
# attr_reader: uidvalidity
87+
# :call-seq: uidvalidity -> nonzero uint32
88+
#
89+
# The UIDVALIDITY of the destination mailbox.
90+
91+
##
92+
# attr_reader: assigned_uids
93+
#
94+
# A SequenceSet with the newly assigned UIDs of the appended messages.
95+
96+
# Returns the number of messages that have been appended.
97+
def size
98+
assigned_uids.count_with_duplicates
99+
end
100+
end
101+
102+
# CopyUIDData represents the ResponseCode#data that accompanies the
103+
# +COPYUID+ {response code}[rdoc-ref:ResponseCode].
104+
#
105+
# A server that supports +UIDPLUS+ (or +IMAP4rev2+) should send CopyUIDData
106+
# in response to
107+
# copy[rdoc-ref:Net::IMAP#copy], {uid_copy}[rdoc-ref:Net::IMAP#uid_copy],
108+
# move[rdoc-ref:Net::IMAP#copy], and {uid_move}[rdoc-ref:Net::IMAP#uid_move]
109+
# commands---unless the destination mailbox reports +UIDNOTSTICKY+.
110+
#
111+
# Note that copy[rdoc-ref:Net::IMAP#copy] and
112+
# {uid_copy}[rdoc-ref:Net::IMAP#uid_copy] return CopyUIDData in their
113+
# TaggedResponse. But move[rdoc-ref:Net::IMAP#copy] and
114+
# {uid_move}[rdoc-ref:Net::IMAP#uid_move] _should_ send CopyUIDData in an
115+
# UntaggedResponse response before sending their TaggedResponse. However
116+
# some servers do send CopyUIDData in the TaggedResponse for +MOVE+
117+
# commands---this complies with the older +UIDPLUS+ specification but is
118+
# discouraged by the +MOVE+ extension and disallowed by +IMAP4rev2+.
119+
#
120+
# == Required capability
121+
# Requires either +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315]]
122+
# or +IMAP4rev2+ capability.
123+
class CopyUIDData < Data.define(:uidvalidity, :source_uids, :assigned_uids)
124+
def initialize(uidvalidity:, source_uids:, assigned_uids:)
125+
uidvalidity = Integer(uidvalidity)
126+
source_uids = SequenceSet[source_uids]
127+
assigned_uids = SequenceSet[assigned_uids]
128+
NumValidator.ensure_nz_number(uidvalidity)
129+
if source_uids.include_star? || assigned_uids.include_star?
130+
raise DataFormatError, "uid-set cannot contain '*'"
131+
elsif source_uids.count_with_duplicates != assigned_uids.count_with_duplicates
132+
raise DataFormatError, "mismatched uid-set sizes for %s and %s" % [
133+
source_uids, assigned_uids
134+
]
135+
end
136+
super
137+
end
138+
139+
##
140+
# attr_reader: uidvalidity
141+
#
142+
# The +UIDVALIDITY+ of the destination mailbox (a nonzero unsigned 32 bit
143+
# integer).
144+
145+
##
146+
# attr_reader: source_uids
147+
#
148+
# A SequenceSet with the original UIDs of the copied or moved messages.
149+
150+
##
151+
# attr_reader: assigned_uids
152+
#
153+
# A SequenceSet with the newly assigned UIDs of the copied or moved
154+
# messages.
155+
156+
# Returns the number of messages that have been copied or moved.
157+
# source_uids and the assigned_uids will both the same number of UIDs.
158+
def size
159+
assigned_uids.count_with_duplicates
160+
end
161+
162+
# :call-seq:
163+
# assigned_uid_for(source_uid) -> uid
164+
# self[source_uid] -> uid
165+
#
166+
# Returns the UID in the destination mailbox for the message that was
167+
# copied from +source_uid+ in the source mailbox.
168+
#
169+
# This is the reverse of #source_uid_for.
170+
#
171+
# Related: source_uid_for, each_uid_pair, uid_mapping
172+
def assigned_uid_for(source_uid)
173+
idx = source_uids.find_ordered_index(source_uid) and
174+
assigned_uids.ordered_at(idx)
175+
end
176+
alias :[] :assigned_uid_for
177+
178+
# :call-seq:
179+
# source_uid_for(assigned_uid) -> uid
180+
#
181+
# Returns the UID in the source mailbox for the message that was copied to
182+
# +assigned_uid+ in the source mailbox.
183+
#
184+
# This is the reverse of #assigned_uid_for.
185+
#
186+
# Related: assigned_uid_for, each_uid_pair, uid_mapping
187+
def source_uid_for(assigned_uid)
188+
idx = assigned_uids.find_ordered_index(assigned_uid) and
189+
source_uids.ordered_at(idx)
190+
end
191+
192+
# Yields a pair of UIDs for each copied message. The first is the
193+
# message's UID in the source mailbox and the second is the UID in the
194+
# destination mailbox.
195+
#
196+
# Returns an enumerator when no block is given.
197+
#
198+
# Please note the warning on uid_mapping before calling methods like
199+
# +to_h+ or +to_a+ on the returned enumerator.
200+
#
201+
# Related: uid_mapping, assigned_uid_for, source_uid_for
202+
def each_uid_pair
203+
return enum_for(__method__) unless block_given?
204+
source_uids.each_ordered_number.lazy
205+
.zip(assigned_uids.each_ordered_number.lazy) do
206+
|source_uid, assigned_uid|
207+
yield source_uid, assigned_uid
208+
end
209+
end
210+
alias each_pair each_uid_pair
211+
alias each each_uid_pair
212+
213+
# :call-seq: uid_mapping -> hash
214+
#
215+
# Returns a hash mapping each source UID to the newly assigned destination
216+
# UID.
217+
#
218+
# <em>*Warning:*</em> The hash that is created may consume _much_ more
219+
# memory than the data used to create it. When handling responses from an
220+
# untrusted server, check #size before calling this method.
221+
#
222+
# Related: each_uid_pair, assigned_uid_for, source_uid_for
223+
def uid_mapping
224+
each_uid_pair.to_h
225+
end
226+
227+
end
228+
63229
end
64230
end

‎test/net/imap/test_uidplus_data.rb

+186
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,189 @@ class TestUIDPlusData < Test::Unit::TestCase
4444
end
4545

4646
end
47+
48+
class TestAppendUIDData < Test::Unit::TestCase
49+
# alias for convenience
50+
AppendUIDData = Net::IMAP::AppendUIDData
51+
SequenceSet = Net::IMAP::SequenceSet
52+
DataFormatError = Net::IMAP::DataFormatError
53+
UINT32_MAX = 2**32 - 1
54+
55+
test "#uidvalidity must be valid nz-number" do
56+
assert_equal 1, AppendUIDData.new(1, 99).uidvalidity
57+
assert_equal UINT32_MAX, AppendUIDData.new(UINT32_MAX, 1).uidvalidity
58+
assert_raise DataFormatError do AppendUIDData.new(0, 1) end
59+
assert_raise DataFormatError do AppendUIDData.new(2**32, 1) end
60+
end
61+
62+
test "#assigned_uids must be a valid uid-set" do
63+
assert_equal SequenceSet[1], AppendUIDData.new(99, "1").assigned_uids
64+
assert_equal SequenceSet[1..9], AppendUIDData.new(1, "1:9").assigned_uids
65+
assert_equal(SequenceSet[UINT32_MAX],
66+
AppendUIDData.new(1, UINT32_MAX.to_s).assigned_uids)
67+
assert_raise DataFormatError do AppendUIDData.new(1, 0) end
68+
assert_raise DataFormatError do AppendUIDData.new(1, "*") end
69+
assert_raise DataFormatError do AppendUIDData.new(1, "1:*") end
70+
end
71+
72+
test "#size returns the number of UIDs" do
73+
assert_equal(10, AppendUIDData.new(1, "1:10").size)
74+
assert_equal(4_000_000_000, AppendUIDData.new(1, 1..4_000_000_000).size)
75+
end
76+
77+
test "#assigned_uids is converted to SequenceSet" do
78+
assert_equal SequenceSet[1], AppendUIDData.new(99, "1").assigned_uids
79+
assert_equal SequenceSet[1..4], AppendUIDData.new(1, [1, 2, 3, 4]).assigned_uids
80+
end
81+
82+
end
83+
84+
class TestCopyUIDData < Test::Unit::TestCase
85+
# alias for convenience
86+
CopyUIDData = Net::IMAP::CopyUIDData
87+
SequenceSet = Net::IMAP::SequenceSet
88+
DataFormatError = Net::IMAP::DataFormatError
89+
UINT32_MAX = 2**32 - 1
90+
91+
test "#uidvalidity must be valid nz-number" do
92+
assert_equal 1, CopyUIDData.new(1, 99, 99).uidvalidity
93+
assert_equal UINT32_MAX, CopyUIDData.new(UINT32_MAX, 1, 1).uidvalidity
94+
assert_raise DataFormatError do CopyUIDData.new(0, 1, 1) end
95+
assert_raise DataFormatError do CopyUIDData.new(2**32, 1, 1) end
96+
end
97+
98+
test "#source_uids must be valid uid-set" do
99+
assert_equal SequenceSet[1], CopyUIDData.new(99, "1", 99).source_uids
100+
assert_equal SequenceSet[5..8], CopyUIDData.new(1, 5..8, 1..4).source_uids
101+
assert_equal(SequenceSet[UINT32_MAX],
102+
CopyUIDData.new(1, UINT32_MAX.to_s, 1).source_uids)
103+
assert_raise DataFormatError do CopyUIDData.new(99, nil, 99) end
104+
assert_raise DataFormatError do CopyUIDData.new(1, 0, 1) end
105+
assert_raise DataFormatError do CopyUIDData.new(1, "*", 1) end
106+
end
107+
108+
test "#assigned_uids must be a valid uid-set" do
109+
assert_equal SequenceSet[1], CopyUIDData.new(99, 1, "1").assigned_uids
110+
assert_equal SequenceSet[1..9], CopyUIDData.new(1, 1..9, "1:9").assigned_uids
111+
assert_equal(SequenceSet[UINT32_MAX],
112+
CopyUIDData.new(1, 1, UINT32_MAX.to_s).assigned_uids)
113+
assert_raise DataFormatError do CopyUIDData.new(1, 1, 0) end
114+
assert_raise DataFormatError do CopyUIDData.new(1, 1, "*") end
115+
assert_raise DataFormatError do CopyUIDData.new(1, 1, "1:*") end
116+
end
117+
118+
test "#size returns the number of UIDs" do
119+
assert_equal(10, CopyUIDData.new(1, "9,8,7,6,1:5,10", "1:10").size)
120+
assert_equal(4_000_000_000,
121+
CopyUIDData.new(
122+
1, "2000000000:4000000000,1:1999999999", 1..4_000_000_000
123+
).size)
124+
end
125+
126+
test "#source_uids and #assigned_uids must be same size" do
127+
assert_raise DataFormatError do CopyUIDData.new(1, 1..5, 1) end
128+
assert_raise DataFormatError do CopyUIDData.new(1, 1, 1..5) end
129+
end
130+
131+
test "#source_uids is converted to SequenceSet" do
132+
assert_equal SequenceSet[1], CopyUIDData.new(99, "1", 99).source_uids
133+
assert_equal SequenceSet[5, 6, 7, 8], CopyUIDData.new(1, 5..8, 1..4).source_uids
134+
end
135+
136+
test "#assigned_uids is converted to SequenceSet" do
137+
assert_equal SequenceSet[1], CopyUIDData.new(99, 1, "1").assigned_uids
138+
assert_equal SequenceSet[1, 2, 3, 4], CopyUIDData.new(1, "1:4", 1..4).assigned_uids
139+
end
140+
141+
test "#uid_mapping maps source_uids to assigned_uids" do
142+
uidplus = CopyUIDData.new(9999, "20:19,500:495", "92:97,101:100")
143+
assert_equal(
144+
{
145+
19 => 92,
146+
20 => 93,
147+
495 => 94,
148+
496 => 95,
149+
497 => 96,
150+
498 => 97,
151+
499 => 100,
152+
500 => 101,
153+
},
154+
uidplus.uid_mapping
155+
)
156+
end
157+
158+
test "#uid_mapping for with source_uids in unsorted order" do
159+
uidplus = CopyUIDData.new(1, "495:500,20:19", "92:97,101:100")
160+
assert_equal(
161+
{
162+
495 => 92,
163+
496 => 93,
164+
497 => 94,
165+
498 => 95,
166+
499 => 96,
167+
500 => 97,
168+
19 => 100,
169+
20 => 101,
170+
},
171+
uidplus.uid_mapping
172+
)
173+
end
174+
175+
test "#assigned_uid_for(source_uid)" do
176+
uidplus = CopyUIDData.new(1, "495:500,20:19", "92:97,101:100")
177+
assert_equal 92, uidplus.assigned_uid_for(495)
178+
assert_equal 93, uidplus.assigned_uid_for(496)
179+
assert_equal 94, uidplus.assigned_uid_for(497)
180+
assert_equal 95, uidplus.assigned_uid_for(498)
181+
assert_equal 96, uidplus.assigned_uid_for(499)
182+
assert_equal 97, uidplus.assigned_uid_for(500)
183+
assert_equal 100, uidplus.assigned_uid_for( 19)
184+
assert_equal 101, uidplus.assigned_uid_for( 20)
185+
end
186+
187+
test "#[](source_uid)" do
188+
uidplus = CopyUIDData.new(1, "495:500,20:19", "92:97,101:100")
189+
assert_equal 92, uidplus[495]
190+
assert_equal 93, uidplus[496]
191+
assert_equal 94, uidplus[497]
192+
assert_equal 95, uidplus[498]
193+
assert_equal 96, uidplus[499]
194+
assert_equal 97, uidplus[500]
195+
assert_equal 100, uidplus[ 19]
196+
assert_equal 101, uidplus[ 20]
197+
end
198+
199+
test "#source_uid_for(assigned_uid)" do
200+
uidplus = CopyUIDData.new(1, "495:500,20:19", "92:97,101:100")
201+
assert_equal 495, uidplus.source_uid_for( 92)
202+
assert_equal 496, uidplus.source_uid_for( 93)
203+
assert_equal 497, uidplus.source_uid_for( 94)
204+
assert_equal 498, uidplus.source_uid_for( 95)
205+
assert_equal 499, uidplus.source_uid_for( 96)
206+
assert_equal 500, uidplus.source_uid_for( 97)
207+
assert_equal 19, uidplus.source_uid_for(100)
208+
assert_equal 20, uidplus.source_uid_for(101)
209+
end
210+
211+
test "#each_uid_pair" do
212+
uidplus = CopyUIDData.new(1, "495:500,20:19", "92:97,101:100")
213+
expected = {
214+
495 => 92,
215+
496 => 93,
216+
497 => 94,
217+
498 => 95,
218+
499 => 96,
219+
500 => 97,
220+
19 => 100,
221+
20 => 101,
222+
}
223+
actual = {}
224+
uidplus.each_uid_pair do |src, dst| actual[src] = dst end
225+
assert_equal expected, actual
226+
assert_equal expected, uidplus.each_uid_pair.to_h
227+
assert_equal expected.to_a, uidplus.each_uid_pair.to_a
228+
assert_equal expected, uidplus.each_pair.to_h
229+
assert_equal expected, uidplus.each.to_h
230+
end
231+
232+
end

0 commit comments

Comments
 (0)