Skip to content

Commit c0c9ef3

Browse files
authored
Merge pull request #69 from nevans/uidplus
Fix some UIDPLUS issues
2 parents 1e80875 + 60e57eb commit c0c9ef3

File tree

5 files changed

+229
-68
lines changed

5 files changed

+229
-68
lines changed

lib/net/imap.rb

Lines changed: 52 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -434,20 +434,22 @@ def login(user, password)
434434
# Sends a SELECT command to select a +mailbox+ so that messages
435435
# in the +mailbox+ can be accessed.
436436
#
437-
# After you have selected a mailbox, you may retrieve the
438-
# number of items in that mailbox from <code>@responses["EXISTS"][-1]</code>,
439-
# and the number of recent messages from <code>@responses["RECENT"][-1]</code>.
440-
# Note that these values can change if new messages arrive
441-
# during a session; see #add_response_handler for a way of
442-
# detecting this event.
437+
# After you have selected a mailbox, you may retrieve the number of items in
438+
# that mailbox from <tt>imap.responses["EXISTS"][-1]</tt>, and the number of
439+
# recent messages from <tt>imap.responses["RECENT"][-1]</tt>. Note that
440+
# these values can change if new messages arrive during a session or when
441+
# existing messages are expunged; see #add_response_handler for a way to
442+
# detect these events.
443443
#
444444
# A Net::IMAP::NoResponseError is raised if the mailbox does not
445445
# exist or is for some reason non-selectable.
446446
#
447-
# If the server supports the [UIDPLUS[https://www.rfc-editor.org/rfc/rfc4315.html]]
448-
# extension it may return an additional "NO" response with a "UIDNOTSTICKY" response code
449-
# indicating that the mailstore does not support persistent UIDs
450-
# [1[https://www.rfc-editor.org/rfc/rfc4315.html#page-4]]:
447+
# ==== Capabilities
448+
#
449+
# If [UIDPLUS[https://www.rfc-editor.org/rfc/rfc4315.html]] is supported,
450+
# the server may return an untagged "NO" response with a "UIDNOTSTICKY"
451+
# response code indicating that the mailstore does not support persistent
452+
# UIDs:
451453
# @responses["NO"].last.code.name == "UIDNOTSTICKY"
452454
def select(mailbox)
453455
synchronize do
@@ -759,22 +761,24 @@ def status(mailbox, attr)
759761
# not exist (it is not created automatically), or if the flags,
760762
# date_time, or message arguments contain errors.
761763
#
762-
# If the server supports the [UIDPLUS[https://www.rfc-editor.org/rfc/rfc4315.html]]
763-
# extension it returns an array with the UIDVALIDITY and the assigned UID of the
764-
# appended message.
764+
# ==== Capabilities
765+
#
766+
# If +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315.html]] is
767+
# supported, the server's response should include a +APPENDUID+ response
768+
# code with the UIDVALIDITY of the destination mailbox and the assigned UID
769+
# of the appended message.
770+
#
771+
#--
772+
# TODO: add MULTIAPPEND support
773+
#++
765774
def append(mailbox, message, flags = nil, date_time = nil)
766775
args = []
767776
if flags
768777
args.push(flags)
769778
end
770779
args.push(date_time) if date_time
771780
args.push(Literal.new(message))
772-
synchronize do
773-
resp = send_command("APPEND", mailbox, *args)
774-
if resp.data.code && resp.data.code.name == "APPENDUID"
775-
return resp.data.code.data
776-
end
777-
end
781+
send_command("APPEND", mailbox, *args)
778782
end
779783

780784
# Sends a CHECK command to request a checkpoint of the currently
@@ -818,8 +822,10 @@ def expunge
818822
# #responses and this method returns them as an array of
819823
# <em>sequence number</em> integers.
820824
#
821-
# ==== Required capability
822-
# +UIDPLUS+ - described in [UIDPLUS[https://www.rfc-editor.org/rfc/rfc4315.html]].
825+
# ==== Capability requirement
826+
#
827+
# +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315.html]] must be
828+
# supported by the server.
823829
def uid_expunge(uid_set)
824830
synchronize do
825831
send_command("UID EXPUNGE", MessageSet.new(uid_set))
@@ -948,14 +954,21 @@ def uid_store(set, attr, flags)
948954
# a number, an array of numbers, or a Range object. The number is
949955
# a message sequence number.
950956
#
951-
# If the server supports the [UIDPLUS[https://www.rfc-editor.org/rfc/rfc4315.html]]
952-
# extension it returns an array with the UIDVALIDITY, the UID set of the source messages
953-
# and the assigned UID set of the copied messages.
957+
# ==== Capabilities
958+
#
959+
# If +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315.html]] is
960+
# supported, the server's response should include a +COPYUID+ response code
961+
# with the UIDVALIDITY of the destination mailbox, the UID set of the source
962+
# messages, and the assigned UID set of the moved messages.
954963
def copy(set, mailbox)
955964
copy_internal("COPY", set, mailbox)
956965
end
957966

958967
# Similar to #copy, but +set+ contains unique identifiers.
968+
#
969+
# ==== Capabilities
970+
#
971+
# +UIDPLUS+ affects #uid_copy the same way it affects #copy.
959972
def uid_copy(set, mailbox)
960973
copy_internal("UID COPY", set, mailbox)
961974
end
@@ -965,18 +978,27 @@ def uid_copy(set, mailbox)
965978
# a number, an array of numbers, or a Range object. The number is
966979
# a message sequence number.
967980
#
968-
# The MOVE extension is described in [EXT-MOVE[https://tools.ietf.org/html/rfc6851]].
981+
# ==== Capabilities requirements
982+
#
983+
# +MOVE+ [RFC6851[https://tools.ietf.org/html/rfc6851]] must be supported by
984+
# the server.
985+
#
986+
# If +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315.html]] is
987+
# also supported, the server's response should include a +COPYUID+ response
988+
# code with the UIDVALIDITY of the destination mailbox, the UID set of the
989+
# source messages, and the assigned UID set of the moved messages.
969990
#
970-
# If the server supports the [UIDPLUS[https://www.rfc-editor.org/rfc/rfc4315.html]]
971-
# extension it returns an array with the UIDVALIDITY, the UID set of the source messages
972-
# and the assigned UID set of the moved messages.
973991
def move(set, mailbox)
974992
copy_internal("MOVE", set, mailbox)
975993
end
976994

977995
# Similar to #move, but +set+ contains unique identifiers.
978996
#
979-
# The MOVE extension is described in [EXT-MOVE[https://tools.ietf.org/html/rfc6851]].
997+
# ==== Capabilities requirements
998+
#
999+
# Same as #move: +MOVE+ [RFC6851[https://tools.ietf.org/html/rfc6851]] must
1000+
# be supported by the server. +UIDPLUS+ also affects #uid_move the same way
1001+
# it affects #move.
9801002
def uid_move(set, mailbox)
9811003
copy_internal("UID MOVE", set, mailbox)
9821004
end
@@ -1432,12 +1454,7 @@ def store_internal(cmd, set, attr, flags)
14321454
end
14331455

14341456
def copy_internal(cmd, set, mailbox)
1435-
synchronize do
1436-
resp = send_command(cmd, MessageSet.new(set), mailbox)
1437-
if resp.data.code && resp.data.code.name == "COPYUID"
1438-
return resp.data.code.data
1439-
end
1440-
end
1457+
send_command(cmd, MessageSet.new(set), mailbox)
14411458
end
14421459

14431460
def sort_internal(cmd, sort_keys, search_keys, charset)

lib/net/imap/response_data.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,65 @@ class ResponseText < Struct.new(:code, :text)
113113
class ResponseCode < Struct.new(:name, :data)
114114
end
115115

116+
# Net::IMAP::UIDPlusData represents the ResponseCode#data that accompanies
117+
# the +APPENDUID+ and +COPYUID+ response codes.
118+
#
119+
# ==== Capability requirement
120+
#
121+
# The +UIDPLUS+ capability[rdoc-ref:Net::IMAP#capability] must be supported.
122+
# A server that supports +UIDPLUS+ should send a UIDPlusData object inside
123+
# every TaggedResponse returned by the append[rdoc-ref:Net::IMAP#append],
124+
# copy[rdoc-ref:Net::IMAP#copy], move[rdoc-ref:Net::IMAP#move], {uid
125+
# copy}[rdoc-ref:Net::IMAP#uid_copy], and {uid
126+
# move}[rdoc-ref:Net::IMAP#uid_move] commands---unless the destination
127+
# mailbox reports +UIDNOTSTICKY+.
128+
#
129+
#--
130+
# TODO: support MULTIAPPEND
131+
#++
132+
#
133+
# ==== References
134+
#
135+
# [UIDPLUS[https://www.rfc-editor.org/rfc/rfc4315.html]]::
136+
# Crispin, M., "Internet Message Access Protocol (IMAP) - UIDPLUS
137+
# extension", RFC 4315, DOI 10.17487/RFC4315, December 2005,
138+
# <https://www.rfc-editor.org/info/rfc4315>.
139+
#
140+
class UIDPlusData < Struct.new(:uidvalidity, :source_uids, :assigned_uids)
141+
##
142+
# method: uidvalidity
143+
# :call-seq: uidvalidity -> nonzero uint32
144+
#
145+
# The UIDVALIDITY of the destination mailbox.
146+
147+
##
148+
# method: source_uids
149+
# :call-seq: source_uids -> nil or an array of nonzero uint32
150+
#
151+
# The UIDs of the copied or moved messages.
152+
#
153+
# Note:: Returns +nil+ for Net::IMAP#append.
154+
155+
##
156+
# method: assigned_uids
157+
# :call-seq: assigned_uids -> an array of nonzero uint32
158+
#
159+
# The newly assigned UIDs of the copied, moved, or appended messages.
160+
#
161+
# Note:: This always returns an array, even when it contains only one UID.
162+
163+
##
164+
# :call-seq: uid_mapping -> nil or a hash
165+
#
166+
# Returns a hash mapping each source UID to the newly assigned destination
167+
# UID.
168+
#
169+
# Note:: Returns +nil+ for Net::IMAP#append.
170+
def uid_mapping
171+
source_uids&.zip(assigned_uids)&.to_h
172+
end
173+
end
174+
116175
# Net::IMAP::MailboxList represents contents of the LIST response.
117176
#
118177
# mailbox_list ::= "(" #("\Marked" / "\Noinferiors" /

lib/net/imap/response_parser.rb

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,11 +1104,8 @@ def resp_text
11041104
# "UNSEEN" SP nz-number /
11051105
# atom [SP 1*<any TEXT-CHAR except "]">]
11061106
#
1107-
# See https://datatracker.ietf.org/doc/html/rfc4315#section-6.4 for UIDPLUS extension
1108-
#
1109-
# resp-code-apnd = "APPENDUID" SP nz-number SP append-uid
1110-
# resp-code-copy = "COPYUID" SP nz-number SP uid-set SP uid-set
1111-
# resp-text-code =/ resp-code-apnd / resp-code-copy / "UIDNOTSTICKY"
1107+
# +UIDPLUS+ ABNF:: https://www.rfc-editor.org/rfc/rfc4315.html#section-4
1108+
# resp-text-code =/ resp-code-apnd / resp-code-copy / "UIDNOTSTICKY"
11121109
def resp_text_code
11131110
token = match(T_ATOM)
11141111
name = token.value.upcase
@@ -1126,19 +1123,9 @@ def resp_text_code
11261123
match(T_SPACE)
11271124
result = ResponseCode.new(name, number)
11281125
when /\A(?:APPENDUID)\z/n
1129-
match(T_SPACE)
1130-
uidvalidity = number
1131-
match(T_SPACE)
1132-
append_uid = number
1133-
result = ResponseCode.new(name, [uidvalidity, append_uid])
1126+
result = ResponseCode.new(name, resp_code_apnd__data)
11341127
when /\A(?:COPYUID)\z/n
1135-
match(T_SPACE)
1136-
uidvalidity = number
1137-
match(T_SPACE)
1138-
from_uid = uid_set
1139-
match(T_SPACE)
1140-
to_uid = uid_set
1141-
result = ResponseCode.new(name, [uidvalidity, from_uid, to_uid])
1128+
result = ResponseCode.new(name, resp_code_copy__data)
11421129
else
11431130
token = lookahead
11441131
if token.symbol == T_SPACE
@@ -1165,6 +1152,34 @@ def charset_list
11651152
result
11661153
end
11671154

1155+
# already matched: "APPENDUID"
1156+
#
1157+
# +UIDPLUS+ ABNF:: https://www.rfc-editor.org/rfc/rfc4315.html#section-4
1158+
# resp-code-apnd = "APPENDUID" SP nz-number SP append-uid
1159+
# append-uid = uniqueid
1160+
# append-uid =/ uid-set
1161+
# ; only permitted if client uses [MULTIAPPEND]
1162+
# ; to append multiple messages.
1163+
#
1164+
# n.b, uniqueid ⊂ uid-set. To avoid inconsistent return types, we always
1165+
# match uid_set even if that returns a single-member array.
1166+
#
1167+
def resp_code_apnd__data
1168+
match(T_SPACE); validity = number
1169+
match(T_SPACE); dst_uids = uid_set # uniqueid ⊂ uid-set
1170+
UIDPlusData.new(validity, nil, dst_uids)
1171+
end
1172+
1173+
# already matched: "COPYUID"
1174+
#
1175+
# resp-code-copy = "COPYUID" SP nz-number SP uid-set SP uid-set
1176+
def resp_code_copy__data
1177+
match(T_SPACE); validity = number
1178+
match(T_SPACE); src_uids = uid_set
1179+
match(T_SPACE); dst_uids = uid_set
1180+
UIDPlusData.new(validity, src_uids, dst_uids)
1181+
end
1182+
11681183
def address_list
11691184
token = lookahead
11701185
if token.symbol == T_NIL
@@ -1350,19 +1365,14 @@ def number
13501365
# uniqueid = nz-number
13511366
# ; Strictly ascending
13521367
def uid_set
1353-
case lookahead.symbol
1354-
when T_NUMBER then [match(T_NUMBER).value.to_i]
1368+
token = match(T_NUMBER, T_ATOM)
1369+
case token.symbol
1370+
when T_NUMBER then [Integer(token.value)]
13551371
when T_ATOM
1356-
match(T_ATOM).value.split(',').flat_map do |element|
1357-
if element.include?(':')
1358-
Range.new(*element.split(':').map(&:to_i)).to_a
1359-
else
1360-
element.to_i
1361-
end
1362-
end
1363-
else
1364-
shift_token
1365-
nil
1372+
token.value.split(",").flat_map {|range|
1373+
range = range.split(":").map {|uniqueid| Integer(uniqueid) }
1374+
range.size == 1 ? range : Range.new(range.min, range.max).to_a
1375+
}
13661376
end
13671377
end
13681378

test/net/imap/test_imap.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -848,16 +848,16 @@ def test_uidplus_responses
848848
849849
hello world
850850
EOF
851-
assert_equal(resp, [38505, 3955])
851+
assert_equal([38505, nil, [3955]], resp.data.code.data.to_a)
852852
resp = imap.uid_copy([3955,3960..3962], 'trash')
853853
assert_equal(requests.pop, "RUBY0002 UID COPY 3955,3960:3962 trash\r\n")
854854
assert_equal(
855-
resp,
856-
[38505, [3955, 3960, 3961, 3962], [3963, 3964, 3965, 3966]]
855+
[38505, [3955, 3960, 3961, 3962], [3963, 3964, 3965, 3966]],
856+
resp.data.code.data.to_a
857857
)
858858
resp = imap.uid_copy(3955, 'trash')
859859
assert_equal(requests.pop, "RUBY0003 UID COPY 3955 trash\r\n")
860-
assert_equal(resp, [38505, [3955], [3967]])
860+
assert_equal([38505, [3955], [3967]], resp.data.code.data.to_a)
861861
imap.select('trash')
862862
assert_equal(
863863
imap.responses["NO"].last.code,

0 commit comments

Comments
 (0)