feat: mail phase 4 - IMAP folder create/delete + auto-sync
- Add imap_create_folder() and imap_delete_folder() service functions - Call IMAP CREATE/DELETE in folder create/delete endpoints - Add auto_sync_all_accounts() background task (every 5 min) - Start auto-sync loop on plugin activation via asyncio.create_task - Frontend: working sync button with isSyncing state and toast feedback - i18n: syncSuccess, syncFailed, syncing, autoSyncEnabled keys (de/en)
This commit is contained in:
@@ -1866,3 +1866,168 @@ async def update_draft(
|
||||
pass
|
||||
|
||||
return mail
|
||||
|
||||
|
||||
# ─── IMAP Folder Create / Delete ───
|
||||
|
||||
|
||||
async def imap_create_folder(
|
||||
db: AsyncSession, account_id: uuid.UUID, folder_name: str, tenant_id: uuid.UUID
|
||||
) -> None:
|
||||
"""Create folder on IMAP server.
|
||||
|
||||
Connects to IMAP, creates folder with CREATE command.
|
||||
Non-critical: errors are logged, DB operation still succeeds.
|
||||
"""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
account = (
|
||||
await db.execute(
|
||||
select(MailAccount).where(
|
||||
and_(MailAccount.id == account_id, MailAccount.tenant_id == tenant_id)
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if not account:
|
||||
logger.warning("imap_create_folder: account %s not found", account_id)
|
||||
return
|
||||
|
||||
password = await get_account_password(account)
|
||||
client = None
|
||||
|
||||
try:
|
||||
client = aioimaplib.IMAP4_SSL(host=account.imap_host, port=account.imap_port)
|
||||
await client.wait_hello_from_server()
|
||||
await client.login(account.username, password)
|
||||
|
||||
resp = await client.create(folder_name)
|
||||
if resp.result != "OK":
|
||||
logger.warning(
|
||||
"imap_create_folder: CREATE failed for %s: %s", folder_name, resp
|
||||
)
|
||||
else:
|
||||
logger.info("imap_create_folder: created folder %s on IMAP", folder_name)
|
||||
|
||||
except Exception as exc:
|
||||
logger.warning("imap_create_folder: failed (non-critical): %s", exc)
|
||||
finally:
|
||||
if client is not None:
|
||||
try:
|
||||
await client.logout()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def imap_delete_folder(
|
||||
db: AsyncSession, folder_id: uuid.UUID, tenant_id: uuid.UUID
|
||||
) -> None:
|
||||
"""Delete folder from IMAP server.
|
||||
|
||||
Connects to IMAP, deletes folder with DELETE command.
|
||||
Non-critical: errors are logged, DB operation still succeeds.
|
||||
"""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
folder = (
|
||||
await db.execute(
|
||||
select(MailFolder).where(
|
||||
and_(MailFolder.id == folder_id, MailFolder.tenant_id == tenant_id)
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if not folder:
|
||||
logger.warning("imap_delete_folder: folder %s not found", folder_id)
|
||||
return
|
||||
|
||||
account = (
|
||||
await db.execute(
|
||||
select(MailAccount).where(
|
||||
and_(
|
||||
MailAccount.id == folder.account_id,
|
||||
MailAccount.tenant_id == tenant_id,
|
||||
)
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if not account:
|
||||
logger.warning("imap_delete_folder: account not found for folder %s", folder_id)
|
||||
return
|
||||
|
||||
password = await get_account_password(account)
|
||||
client = None
|
||||
|
||||
try:
|
||||
client = aioimaplib.IMAP4_SSL(host=account.imap_host, port=account.imap_port)
|
||||
await client.wait_hello_from_server()
|
||||
await client.login(account.username, password)
|
||||
|
||||
resp = await client.delete(folder.imap_name)
|
||||
if resp.result != "OK":
|
||||
logger.warning(
|
||||
"imap_delete_folder: DELETE failed for %s: %s", folder.imap_name, resp
|
||||
)
|
||||
else:
|
||||
logger.info("imap_delete_folder: deleted folder %s on IMAP", folder.imap_name)
|
||||
|
||||
except Exception as exc:
|
||||
logger.warning("imap_delete_folder: failed (non-critical): %s", exc)
|
||||
finally:
|
||||
if client is not None:
|
||||
try:
|
||||
await client.logout()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ─── Auto-Sync ───
|
||||
|
||||
|
||||
async def auto_sync_all_accounts() -> None:
|
||||
"""Auto-sync all active mail accounts.
|
||||
|
||||
Called periodically by the background scheduler.
|
||||
Iterates all active mail accounts and syncs each one.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from app.core.db import get_session_factory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
factory = get_session_factory()
|
||||
async with factory() as db:
|
||||
accounts = (
|
||||
await db.execute(
|
||||
select(MailAccount).where(MailAccount.is_active.is_(True))
|
||||
)
|
||||
).scalars().all()
|
||||
|
||||
if not accounts:
|
||||
return
|
||||
|
||||
logger.info("auto_sync_all_accounts: syncing %d active account(s)", len(accounts))
|
||||
|
||||
for account in accounts:
|
||||
try:
|
||||
result = await imap_sync_account(db, account.id, account.tenant_id)
|
||||
logger.info(
|
||||
"auto_sync_all_accounts: synced account %s (%s): %s",
|
||||
account.id,
|
||||
account.username,
|
||||
result,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"auto_sync_all_accounts: failed for account %s: %s",
|
||||
account.id,
|
||||
exc,
|
||||
)
|
||||
# commit per-account so partial progress is saved
|
||||
try:
|
||||
await db.commit()
|
||||
except Exception:
|
||||
await db.rollback()
|
||||
|
||||
Reference in New Issue
Block a user