Skip to content

Commit 3b57605

Browse files
hoffiStefan Hoffmann
authored andcommitted
Parse UIDPLUS responses and use them in the relevant commands as return values
1 parent 34564ad commit 3b57605

File tree

3 files changed

+115
-2
lines changed

3 files changed

+115
-2
lines changed

lib/net/imap.rb

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,11 @@ def login(user, password)
443443
#
444444
# A Net::IMAP::NoResponseError is raised if the mailbox does not
445445
# exist or is for some reason non-selectable.
446+
#
447+
# If the server supports the UIDPLUS extension it may return an additional
448+
# "NO" response with a "UIDNOTSTICKY" response code indicating that the
449+
# mailstore does not support persistent UIDs:
450+
# +@responses["NO"].last.code.name == "UIDNOTSTICKY"+
446451
def select(mailbox)
447452
synchronize do
448453
@responses.clear
@@ -752,14 +757,22 @@ def status(mailbox, attr)
752757
# A Net::IMAP::NoResponseError is raised if the mailbox does
753758
# not exist (it is not created automatically), or if the flags,
754759
# date_time, or message arguments contain errors.
760+
#
761+
# If the server supports the UIDPLUS extension it returns an array
762+
# with the UIDVALIDITY and the assigned UID of the appended message.
755763
def append(mailbox, message, flags = nil, date_time = nil)
756764
args = []
757765
if flags
758766
args.push(flags)
759767
end
760768
args.push(date_time) if date_time
761769
args.push(Literal.new(message))
762-
send_command("APPEND", mailbox, *args)
770+
synchronize do
771+
resp = send_command("APPEND", mailbox, *args)
772+
if resp.data.code && resp.data.code.name == "APPENDUID"
773+
return resp.data.code.data
774+
end
775+
end
763776
end
764777

765778
# Sends a CHECK command to request a checkpoint of the currently
@@ -915,6 +928,10 @@ def uid_store(set, attr, flags)
915928
# of the specified destination +mailbox+. The +set+ parameter is
916929
# a number, an array of numbers, or a Range object. The number is
917930
# a message sequence number.
931+
#
932+
# If the server supports the UIDPLUS extension it returns an array with
933+
# the UIDVALIDITY, the UID set of the source messages and the assigned
934+
# UID set of the copied messages.
918935
def copy(set, mailbox)
919936
copy_internal("COPY", set, mailbox)
920937
end
@@ -930,6 +947,10 @@ def uid_copy(set, mailbox)
930947
# a message sequence number.
931948
#
932949
# The MOVE extension is described in [EXT-MOVE[https://tools.ietf.org/html/rfc6851]].
950+
#
951+
# If the server supports the UIDPLUS extension it returns an array with
952+
# the UIDVALIDITY, the UID set of the source messages and the assigned
953+
# UID set of the moved messages.
933954
def move(set, mailbox)
934955
copy_internal("MOVE", set, mailbox)
935956
end
@@ -1392,7 +1413,12 @@ def store_internal(cmd, set, attr, flags)
13921413
end
13931414

13941415
def copy_internal(cmd, set, mailbox)
1395-
send_command(cmd, MessageSet.new(set), mailbox)
1416+
synchronize do
1417+
resp = send_command(cmd, MessageSet.new(set), mailbox)
1418+
if resp.data.code && resp.data.code.name == "COPYUID"
1419+
return resp.data.code.data
1420+
end
1421+
end
13961422
end
13971423

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

lib/net/imap/response_parser.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,6 +1103,12 @@ def resp_text
11031103
# "UIDNEXT" SP nz-number / "UIDVALIDITY" SP nz-number /
11041104
# "UNSEEN" SP nz-number /
11051105
# atom [SP 1*<any TEXT-CHAR except "]">]
1106+
#
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"
11061112
def resp_text_code
11071113
token = match(T_ATOM)
11081114
name = token.value.upcase
@@ -1119,6 +1125,20 @@ def resp_text_code
11191125
when /\A(?:UIDVALIDITY|UIDNEXT|UNSEEN)\z/n
11201126
match(T_SPACE)
11211127
result = ResponseCode.new(name, number)
1128+
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])
1134+
when /\A(?:COPYUID)\z/n
1135+
match(T_SPACE)
1136+
uidvalidity = number
1137+
match(T_SPACE)
1138+
from_uid = set
1139+
match(T_SPACE)
1140+
to_uid = set
1141+
result = ResponseCode.new(name, [uidvalidity, from_uid, to_uid])
11221142
else
11231143
token = lookahead
11241144
if token.symbol == T_SPACE
@@ -1321,6 +1341,17 @@ def number
13211341
return token.value.to_i
13221342
end
13231343

1344+
def set
1345+
token = match(T_ATOM)
1346+
token.value.split(',').flat_map do |element|
1347+
if element.include?(':')
1348+
Range.new(*element.split(':').map(&:to_i)).to_a
1349+
else
1350+
element.to_i
1351+
end
1352+
end
1353+
end
1354+
13241355
def nil_atom
13251356
match(T_NIL)
13261357
return nil

test/net/imap/test_imap.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,62 @@ def test_uid_expunge
808808
end
809809
end
810810

811+
def test_uidplus_responses
812+
server = create_tcp_server
813+
port = server.addr[1]
814+
requests = []
815+
start_server do
816+
sock = server.accept
817+
begin
818+
sock.print("* OK test server\r\n")
819+
line = sock.gets
820+
size = line.slice(/{(\d+)}\r\n/, 1).to_i
821+
sock.print("+ Ready for literal data\r\n")
822+
sock.read(size)
823+
sock.gets
824+
sock.print("RUBY0001 OK [APPENDUID 38505 3955] APPEND completed\r\n")
825+
requests.push(sock.gets)
826+
sock.print("RUBY0002 OK [COPYUID 38505 3955,3960:3962 3963:3966] " \
827+
"COPY completed\r\n")
828+
sock.gets
829+
sock.print("* NO [UIDNOTSTICKY] Non-persistent UIDs\r\n")
830+
sock.print("RUBY0003 OK SELECT completed\r\n")
831+
sock.gets
832+
sock.print("* BYE terminating connection\r\n")
833+
sock.print("RUBY0004 OK LOGOUT completed\r\n")
834+
ensure
835+
sock.close
836+
server.close
837+
end
838+
end
839+
840+
begin
841+
imap = Net::IMAP.new(server_addr, :port => port)
842+
resp = imap.append("inbox", <<~EOF.gsub(/\n/, "\r\n"), [:Seen], Time.now)
843+
Subject: hello
844+
845+
846+
847+
hello world
848+
EOF
849+
assert_equal(resp, [38505, 3955])
850+
resp = imap.uid_copy([3955,3960..3962], 'trash')
851+
assert_equal(requests.pop, "RUBY0002 UID COPY 3955,3960:3962 trash\r\n")
852+
assert_equal(
853+
resp,
854+
[38505, [3955, 3960, 3961, 3962], [3963, 3964, 3965, 3966]]
855+
)
856+
imap.select('trash')
857+
assert_equal(
858+
imap.responses["NO"].last.code,
859+
Net::IMAP::ResponseCode.new('UIDNOTSTICKY', nil)
860+
)
861+
imap.logout
862+
ensure
863+
imap.disconnect if imap
864+
end
865+
end
866+
811867
private
812868

813869
def imaps_test

0 commit comments

Comments
 (0)