diff --git a/app/plugins/builtins/mail/services.py b/app/plugins/builtins/mail/services.py index 303a824..6a88a27 100644 --- a/app/plugins/builtins/mail/services.py +++ b/app/plugins/builtins/mail/services.py @@ -665,12 +665,33 @@ async def imap_sync_folder( ).scalar_one_or_none() if existing_mail: - # If mail has deleted_at, it was deleted in LeoCRM. - # DO NOT restore it — keep it deleted and let the sync queue - # retry the IMAP delete/move. This prevents deleted mails from - # reappearing in the inbox. + # Bidirectional sync logic: + # - Mail has deleted_at AND is still on IMAP server + # - If pending delete in sync queue → our IMAP delete failed, DON'T restore + # - If no pending delete → another program restored it, DO restore + # - Mail has no deleted_at → normal update if existing_mail.deleted_at is not None: - continue + # Check if there's a pending delete in the sync queue + from app.plugins.builtins.mail.models import MailSyncQueue + pending_delete = ( + await db.execute( + select(MailSyncQueue).where( + and_( + MailSyncQueue.mail_id == existing_mail.id, + MailSyncQueue.operation == "delete", + MailSyncQueue.status == "pending", + ) + ) + ) + ).scalar_one_or_none() + if pending_delete: + # Our IMAP delete failed and is retrying — don't restore + logger.info("imap_sync_folder: skipping mail %s (deleted, pending IMAP delete in queue)", existing_mail.id) + continue + else: + # No pending delete → another program restored the mail → restore in DB + existing_mail.deleted_at = None + logger.info("imap_sync_folder: restored mail %s (no pending delete, another program restored it)", existing_mail.id) # Update folder_id and imap_uid if mail moved to a different folder if existing_mail.folder_id != folder.id: existing_mail.folder_id = folder.id @@ -1164,11 +1185,30 @@ async def imap_sync_account( ).scalar_one_or_none() if existing_mail: - # If mail has deleted_at, it was deleted in LeoCRM. - # DO NOT restore it — keep it deleted and let the sync queue - # retry the IMAP delete/move. + # Bidirectional sync logic: + # - Mail has deleted_at AND is still on IMAP server + # - If pending delete in sync queue → our IMAP delete failed, DON'T restore + # - If no pending delete → another program restored it, DO restore + # - Mail has no deleted_at → normal update if existing_mail.deleted_at is not None: - continue + from app.plugins.builtins.mail.models import MailSyncQueue + pending_delete = ( + await db.execute( + select(MailSyncQueue).where( + and_( + MailSyncQueue.mail_id == existing_mail.id, + MailSyncQueue.operation == "delete", + MailSyncQueue.status == "pending", + ) + ) + ) + ).scalar_one_or_none() + if pending_delete: + logger.info("imap_sync_account: skipping mail %s (deleted, pending IMAP delete in queue)", existing_mail.id) + continue + else: + existing_mail.deleted_at = None + logger.info("imap_sync_account: restored mail %s (no pending delete, another program restored it)", existing_mail.id) # Track folder moves: update folder_id and imap_uid if the # mail now appears in a different IMAP folder. if existing_mail.folder_id != folder.id: