fix: IMAP sync overhaul - move to Trash, fix func import, fix count queries
- Fix missing func import (imap_sync_folder was crashing silently)
- imap_delete_mail now moves to Trash folder instead of permanent delete
- Add permanent parameter: permanent=True for Trash->delete, False for Inbox->Trash
- delete_mail route: move to Trash if not in Trash, permanent delete if in Trash
- empty_folder: move to Trash or permanent delete if already in Trash
- Add Papierkorb/Trash folder to create_mail_account
- Fix not Mail.is_seen -> Mail.is_seen.is_(False) (SQLAlchemy bug)
- Fix select(text("COUNT(*)")) -> select(func.count()).select_from(Mail)
- process_sync_queue reads permanent flag from payload
This commit is contained in:
@@ -597,9 +597,10 @@ async def empty_folder(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(require_permission("mail:delete")),
|
||||
):
|
||||
"""Soft-delete all mails in a folder (sets deleted_at = now()).
|
||||
"""Empty a folder: move all mails to Trash, or permanent delete if already in Trash.
|
||||
|
||||
The mails remain in the database but are hidden from the normal list.
|
||||
If the folder IS the Trash folder, mails are permanently deleted (deleted_at + IMAP EXPUNGE).
|
||||
If the folder is NOT Trash, mails are moved to Trash (folder_id changed + IMAP MOVE).
|
||||
Returns the number of mails that were emptied.
|
||||
"""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
@@ -614,7 +615,26 @@ async def empty_folder(
|
||||
raise HTTPException(404, detail={"detail": "Folder not found", "code": "not_found"})
|
||||
account = await _get_account(db, folder.account_id, tenant_id, user_id)
|
||||
await _check_delegate_access(db, account, user_id, "delete")
|
||||
# Soft-delete all non-deleted mails in this folder
|
||||
|
||||
# Check if this folder IS the Trash folder
|
||||
is_trash = 'trash' in folder.imap_name.lower() or 'papierkorb' in folder.name.lower() or 'trash' in folder.name.lower()
|
||||
|
||||
# Find Trash folder for non-trash emptying
|
||||
trash_folder = None
|
||||
if not is_trash:
|
||||
all_folders = (
|
||||
await db.execute(
|
||||
select(MailFolder).where(
|
||||
and_(MailFolder.account_id == folder.account_id, MailFolder.tenant_id == tenant_id)
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
for f in all_folders:
|
||||
if 'trash' in f.imap_name.lower() or 'papierkorb' in f.name.lower() or 'trash' in f.name.lower():
|
||||
trash_folder = f
|
||||
break
|
||||
|
||||
# Get all non-deleted mails in this folder
|
||||
result = await db.execute(
|
||||
select(Mail).where(
|
||||
and_(
|
||||
@@ -627,7 +647,44 @@ async def empty_folder(
|
||||
mails = result.scalars().all()
|
||||
now = datetime.now(UTC)
|
||||
for mail in mails:
|
||||
mail.deleted_at = now
|
||||
if is_trash or not trash_folder:
|
||||
# Permanent delete
|
||||
mail.deleted_at = now
|
||||
try:
|
||||
await mail_services.imap_delete_mail(db, mail.id, tenant_id, permanent=True)
|
||||
except Exception as exc:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning(
|
||||
"empty_folder: IMAP permanent delete failed for mail %s: %s, queuing for retry", mail.id, exc
|
||||
)
|
||||
queue_entry = MailSyncQueue(
|
||||
tenant_id=tenant_id,
|
||||
mail_id=mail.id,
|
||||
operation="delete",
|
||||
payload={"permanent": True},
|
||||
status="pending",
|
||||
last_error=str(exc),
|
||||
)
|
||||
db.add(queue_entry)
|
||||
else:
|
||||
# Move to Trash folder (keep visible in Trash)
|
||||
mail.folder_id = trash_folder.id
|
||||
try:
|
||||
await mail_services.imap_delete_mail(db, mail.id, tenant_id, permanent=False)
|
||||
except Exception as exc:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning(
|
||||
"empty_folder: IMAP move to Trash failed for mail %s: %s, queuing for retry", mail.id, exc
|
||||
)
|
||||
queue_entry = MailSyncQueue(
|
||||
tenant_id=tenant_id,
|
||||
mail_id=mail.id,
|
||||
operation="delete",
|
||||
payload={"permanent": False},
|
||||
status="pending",
|
||||
last_error=str(exc),
|
||||
)
|
||||
db.add(queue_entry)
|
||||
await db.flush()
|
||||
return {"emptied_count": len(mails)}
|
||||
|
||||
@@ -1555,25 +1612,78 @@ async def delete_mail(
|
||||
raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
|
||||
account = await _get_account(db, mail.account_id, tenant_id, user_id)
|
||||
await _check_delegate_access(db, account, user_id, "delete")
|
||||
# Soft-delete: set deleted_at = now()
|
||||
mail.deleted_at = datetime.now(UTC)
|
||||
await db.flush()
|
||||
# IMAP delete: queue for retry if it fails
|
||||
try:
|
||||
await mail_services.imap_delete_mail(db, m_id, tenant_id)
|
||||
except Exception as exc:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning("IMAP delete failed for mail %s: %s, queuing for retry", m_id, exc)
|
||||
queue_entry = MailSyncQueue(
|
||||
tenant_id=tenant_id,
|
||||
mail_id=m_id,
|
||||
operation="delete",
|
||||
payload={},
|
||||
status="pending",
|
||||
last_error=str(exc),
|
||||
|
||||
# Find the Trash folder for this account
|
||||
trash_folder = None
|
||||
all_folders = (
|
||||
await db.execute(
|
||||
select(MailFolder).where(
|
||||
and_(MailFolder.account_id == mail.account_id, MailFolder.tenant_id == tenant_id)
|
||||
)
|
||||
)
|
||||
db.add(queue_entry)
|
||||
).scalars().all()
|
||||
for f in all_folders:
|
||||
if 'trash' in f.imap_name.lower() or 'trash' in f.name.lower() or 'papierkorb' in f.name.lower():
|
||||
trash_folder = f
|
||||
break
|
||||
|
||||
if trash_folder and mail.folder_id == trash_folder.id:
|
||||
# Mail is already in Trash → permanent delete
|
||||
mail.deleted_at = datetime.now(UTC)
|
||||
await db.flush()
|
||||
try:
|
||||
await mail_services.imap_delete_mail(db, m_id, tenant_id, permanent=True)
|
||||
except Exception as exc:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning("IMAP permanent delete failed for mail %s: %s, queuing for retry", m_id, exc)
|
||||
queue_entry = MailSyncQueue(
|
||||
tenant_id=tenant_id,
|
||||
mail_id=m_id,
|
||||
operation="delete",
|
||||
payload={"permanent": True},
|
||||
status="pending",
|
||||
last_error=str(exc),
|
||||
)
|
||||
db.add(queue_entry)
|
||||
await db.flush()
|
||||
elif trash_folder:
|
||||
# Mail is NOT in Trash → move to Trash folder
|
||||
mail.folder_id = trash_folder.id
|
||||
await db.flush()
|
||||
try:
|
||||
await mail_services.imap_delete_mail(db, m_id, tenant_id, permanent=False)
|
||||
except Exception as exc:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning("IMAP move to Trash failed for mail %s: %s, queuing for retry", m_id, exc)
|
||||
queue_entry = MailSyncQueue(
|
||||
tenant_id=tenant_id,
|
||||
mail_id=m_id,
|
||||
operation="delete",
|
||||
payload={"permanent": False},
|
||||
status="pending",
|
||||
last_error=str(exc),
|
||||
)
|
||||
db.add(queue_entry)
|
||||
await db.flush()
|
||||
else:
|
||||
# No Trash folder found → permanent delete
|
||||
mail.deleted_at = datetime.now(UTC)
|
||||
await db.flush()
|
||||
try:
|
||||
await mail_services.imap_delete_mail(db, m_id, tenant_id, permanent=True)
|
||||
except Exception as exc:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning("IMAP delete failed for mail %s: %s, queuing for retry", m_id, exc)
|
||||
queue_entry = MailSyncQueue(
|
||||
tenant_id=tenant_id,
|
||||
mail_id=m_id,
|
||||
operation="delete",
|
||||
payload={"permanent": True},
|
||||
status="pending",
|
||||
last_error=str(exc),
|
||||
)
|
||||
db.add(queue_entry)
|
||||
await db.flush()
|
||||
|
||||
|
||||
@router.post("/{mail_id}/move")
|
||||
|
||||
Reference in New Issue
Block a user