Skip to content

Commit 7cb6b97

Browse files
committed
Use move when available
1 parent c085d5f commit 7cb6b97

5 files changed

Lines changed: 68 additions & 4 deletions

File tree

inbox/actions/backends/generic.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,12 @@ def remote_move( # type: ignore[no-untyped-def]
111111

112112
for folder_name, uids in uids_for_message.items():
113113
crispin_client.select_folder_if_necessary(folder_name, uidvalidity_cb)
114-
crispin_client.conn.copy(uids, destination)
115-
crispin_client.delete_uids(uids)
114+
115+
if crispin_client.move_supported():
116+
crispin_client.conn.move(uids, destination)
117+
else:
118+
crispin_client.conn.copy(uids, destination)
119+
crispin_client.delete_uids(uids)
116120

117121

118122
def remote_create_folder( # type: ignore[no-untyped-def]

inbox/crispin.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,10 @@ def idle_supported(self) -> bool:
874874

875875
return b"IDLE" in self.conn.capabilities()
876876

877+
def move_supported(self) -> bool:
878+
interruptible_threading.check_interrupted()
879+
return b"MOVE" in self.conn.capabilities()
880+
877881
def search_uids(self, criteria: list[str]) -> Iterable[int]:
878882
"""
879883
Find UIDs in this folder matching the criteria. See

inbox/util/testutils.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,21 @@ def copy( # type: ignore[no-untyped-def]
263263
self, matching_uids, folder_name
264264
) -> None:
265265
"""
266-
Note: _moves_ one or more messages from the currently selected folder
267-
to folder_name
266+
Copies one or more messages from the currently selected folder
267+
to folder_name.
268+
269+
Note: Also deletes from source to simulate existing test expectations.
270+
"""
271+
for u in matching_uids:
272+
self._data[folder_name][u] = self._data[self.selected_folder][u]
273+
self.delete_messages(matching_uids)
274+
275+
def move( # type: ignore[no-untyped-def]
276+
self, matching_uids, folder_name
277+
) -> None:
278+
"""
279+
Atomically moves one or more messages from the currently selected folder
280+
to folder_name (RFC 6851).
268281
"""
269282
for u in matching_uids:
270283
self._data[folder_name][u] = self._data[self.selected_folder][u]

pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[pytest]
2+
testpaths = tests
23
norecursedirs = inbox tests/imap/network tests/data
34
timeout = 60
45

tests/imap/test_actions.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
delete_label,
1414
mark_starred,
1515
mark_unread,
16+
move,
1617
save_draft,
1718
update_draft,
1819
update_folder,
@@ -275,3 +276,44 @@ def test_failed_event_creation(
275276
assert all(a.status == "failed" for a in q)
276277

277278
service.stop()
279+
280+
281+
def test_move_uses_imap_move_when_supported(
282+
db, default_account, message, folder, mock_imapclient
283+
) -> None:
284+
"""Test that IMAP MOVE command is used when the server supports it."""
285+
mock_imapclient.add_folder_data(folder.name, {})
286+
mock_imapclient.add_folder_data("Archive", {})
287+
mock_imapclient.capabilities = mock.Mock(return_value=[b"IMAP4rev1", b"MOVE"])
288+
mock_imapclient.move = mock.Mock()
289+
mock_imapclient.copy = mock.Mock()
290+
mock_imapclient.delete_messages = mock.Mock()
291+
add_fake_imapuid(db.session, default_account.id, message, folder, 42)
292+
293+
with writable_connection_pool(default_account.id).get() as crispin_client:
294+
move(crispin_client, default_account.id, message.id, {"destination": "Archive"})
295+
296+
mock_imapclient.move.assert_called_once_with([42], "Archive")
297+
mock_imapclient.copy.assert_not_called()
298+
299+
300+
def test_move_falls_back_to_copy_delete_when_move_not_supported(
301+
db, default_account, message, folder, mock_imapclient
302+
) -> None:
303+
"""Test fallback to COPY+DELETE when MOVE is not supported."""
304+
mock_imapclient.add_folder_data(folder.name, {})
305+
mock_imapclient.add_folder_data("Archive", {})
306+
mock_imapclient.capabilities = mock.Mock(return_value=[b"IMAP4rev1"])
307+
mock_imapclient.move = mock.Mock()
308+
mock_imapclient.copy = mock.Mock()
309+
mock_imapclient.delete_messages = mock.Mock()
310+
mock_imapclient.expunge = mock.Mock()
311+
add_fake_imapuid(db.session, default_account.id, message, folder, 42)
312+
313+
with writable_connection_pool(default_account.id).get() as crispin_client:
314+
move(crispin_client, default_account.id, message.id, {"destination": "Archive"})
315+
316+
mock_imapclient.move.assert_not_called()
317+
mock_imapclient.copy.assert_called_once_with([42], "Archive")
318+
# delete_uids converts UIDs to strings before calling delete_messages
319+
mock_imapclient.delete_messages.assert_called_once_with(["42"], silent=True)

0 commit comments

Comments
 (0)