diff --git a/app/module/mail/ModuleMail.py b/app/module/mail/ModuleMail.py index a5d9ee16..bba80776 100644 --- a/app/module/mail/ModuleMail.py +++ b/app/module/mail/ModuleMail.py @@ -1641,7 +1641,7 @@ def _action_move(self, client: ClientMailServer, folder_name: str, mail_uid: str raise RequestException("Missing or invalid destination folder for move action", err.ERROR_MISSING_ACTION_DATA) client.copy_mail_to_mailbox(folder_name, mail_uid, destination) - client.add_flags_to_mail(folder_name, mail_uid, ['\\Deleted']) + self._flag_deleted_and_expunge(client, folder_name, mail_uid) return {"action": "move", "mail_uid": mail_uid, "from_folder": folder_name, "to_folder": destination} @@ -1658,7 +1658,7 @@ def _action_spam(self, client: ClientMailServer, folder_name: str, mail_uid: str """ junk_folder = self.domain_mail_folder_name.get(cs.MAIL_FOLDER_JUNK, "Junk") client.copy_mail_to_mailbox(folder_name, mail_uid, junk_folder, create_dest=True) - client.add_flags_to_mail(folder_name, mail_uid, ['\\Deleted']) + self._flag_deleted_and_expunge(client, folder_name, mail_uid) return {"action": "spam", "mail_uid": mail_uid, "moved_to": junk_folder} def _action_ham(self, client: ClientMailServer, folder_name: str, mail_uid: str|list[str]) -> dict[str, Any]: @@ -1675,7 +1675,7 @@ def _action_ham(self, client: ClientMailServer, folder_name: str, mail_uid: str| inbox_folder = self.domain_mail_folder_name.get(cs.MAIL_FOLDER_INBOX, "INBOX") junk_folder = self.domain_mail_folder_name.get(cs.MAIL_FOLDER_JUNK, "Junk") client.copy_mail_to_mailbox(junk_folder, mail_uid, inbox_folder) - client.add_flags_to_mail(folder_name, mail_uid, ['\\Deleted']) + self._flag_deleted_and_expunge(client, folder_name, mail_uid) return {"action": "ham", "mail_uid": mail_uid, "moved_to": inbox_folder} @@ -1699,6 +1699,18 @@ def _action_copy(self, client: ClientMailServer, folder_name: str, mail_uid: str return {"action": "copy", "mail_uid": mail_uid, "from_folder": folder_name, "to_folder": destination} + def _flag_deleted_and_expunge( + self, client: ClientMailServer, folder_name: str, mail_uid: str|list[str] + ) -> None: + """Flag mails as deleted then expunge the source folder. + + IMAP COPY + ``\\Deleted`` is not a move until EXPUNGE: without it the + original stays visible in the source folder (and a later move copies it + again, producing duplicates). + """ + client.add_flags_to_mail(folder_name, mail_uid, ['\\Deleted']) + client.expunge_folder(folder_name, do_children=False) + def _action_delete(self, client: ClientMailServer, folder_name: str, mail_uid: str|list[str], account_id: str) -> dict[str, Any]: """Delete a mail or a list of mails, honoring the user's ``SOGO_U_MAIL_DELETE_BEHAVIOR`` preference. diff --git a/tests/test_module/test_mail/test_moduleMail.py b/tests/test_module/test_mail/test_moduleMail.py index d7bbbcd5..caeccb13 100644 --- a/tests/test_module/test_mail/test_moduleMail.py +++ b/tests/test_module/test_mail/test_moduleMail.py @@ -44,6 +44,7 @@ def __init__(self): self.add_flags_calls = [] self.remove_flags_calls = [] self.delete_mails_by_uid_calls = [] + self.expunge_folder_calls = [] self.set_acl_calls = [] self.delete_acl_calls = [] @@ -66,7 +67,8 @@ def delete_folder(self, folder_path, do_children=True): if self.delete_folder_result is not None: raise self.delete_folder_result - def expunge_folder(self, folder_path, do_subfolders=True): + def expunge_folder(self, folder_path, do_children=True): + self.expunge_folder_calls.append((folder_path, do_children)) return self.expunge_folder_result def purge_folder(self, folder_path, before_date=None, do_children=False, permanently=False): @@ -585,6 +587,7 @@ def test_perform_mail_action_move_success(monkeypatch): assert result["to_folder"] == "Archive" assert ("INBOX", "42", "Archive") in fake_client.copy_mail_to_mailbox_calls assert ("INBOX", "42", ['\\Deleted']) in fake_client.add_flags_calls + assert ("INBOX", False) in fake_client.expunge_folder_calls def test_perform_mail_action_move_missing_destination(monkeypatch): @@ -610,6 +613,7 @@ def test_perform_mail_action_spam_success(monkeypatch): assert result["moved_to"] == "Junk" assert ("INBOX", "42", "Junk") in fake_client.copy_mail_to_mailbox_calls assert ("INBOX", "42", ['\\Deleted']) in fake_client.add_flags_calls + assert ("INBOX", False) in fake_client.expunge_folder_calls def test_perform_mail_action_ham_success(monkeypatch): @@ -625,6 +629,7 @@ def test_perform_mail_action_ham_success(monkeypatch): assert result["moved_to"] == "INBOX" assert ("Junk", "42", "INBOX") in fake_client.copy_mail_to_mailbox_calls assert ("Junk", "42", ['\\Deleted']) in fake_client.add_flags_calls + assert ("Junk", False) in fake_client.expunge_folder_calls def test_perform_mail_action_copy_success(monkeypatch): @@ -641,6 +646,7 @@ def test_perform_mail_action_copy_success(monkeypatch): assert ("INBOX", "42", "Archive") in fake_client.copy_mail_to_mailbox_calls # Copy should NOT delete the original mail assert ("INBOX", "42", ['\\Deleted']) not in fake_client.add_flags_calls + assert fake_client.expunge_folder_calls == [] def test_perform_mail_action_copy_missing_destination(monkeypatch): @@ -744,6 +750,7 @@ def test_perform_mail_batch_action_move_success(monkeypatch): assert result["to_folder"] == "Archive" assert ("INBOX", ["42", "43"], "Archive") in fake_client.copy_mail_to_mailbox_calls assert ("INBOX", ["42", "43"], ['\\Deleted']) in fake_client.add_flags_calls + assert ("INBOX", False) in fake_client.expunge_folder_calls def test_perform_mail_batch_action_move_missing_destination(monkeypatch): @@ -769,6 +776,7 @@ def test_perform_mail_batch_action_spam_success(monkeypatch): assert result["moved_to"] == "Junk" assert ("INBOX", ["42", "43"], "Junk") in fake_client.copy_mail_to_mailbox_calls assert ("INBOX", ["42", "43"], ['\\Deleted']) in fake_client.add_flags_calls + assert ("INBOX", False) in fake_client.expunge_folder_calls def test_perform_mail_batch_action_ham_success(monkeypatch): @@ -784,6 +792,7 @@ def test_perform_mail_batch_action_ham_success(monkeypatch): assert result["moved_to"] == "INBOX" assert ("Junk", ["42", "43"], "INBOX") in fake_client.copy_mail_to_mailbox_calls assert ("Junk", ["42", "43"], ['\\Deleted']) in fake_client.add_flags_calls + assert ("Junk", False) in fake_client.expunge_folder_calls def test_perform_mail_batch_action_copy_success(monkeypatch): @@ -800,6 +809,7 @@ def test_perform_mail_batch_action_copy_success(monkeypatch): assert ("INBOX", ["42", "43"], "Archive") in fake_client.copy_mail_to_mailbox_calls # Copy should NOT delete the original mails assert ("INBOX", ["42", "43"], ['\\Deleted']) not in fake_client.add_flags_calls + assert fake_client.expunge_folder_calls == [] def test_perform_mail_batch_action_copy_missing_destination(monkeypatch):