From c736afffb03949c9d7ceeda0db5ac2189de580a9 Mon Sep 17 00:00:00 2001 From: rustmailer Date: Sun, 7 Jun 2026 15:34:38 +0800 Subject: [PATCH] fix: account deletion times out #291 --- crates/admin/src/meta.rs | 1 + crates/core/src/account/migration.rs | 51 ++++++++++++++++--- crates/core/src/account/view.rs | 2 + crates/core/src/cache/imap/task.rs | 7 +++ web/src/api/account/api.ts | 1 + .../components/data-table-row-actions.tsx | 7 ++- .../accounts/components/delete-dialog.tsx | 4 +- .../accounts/components/enable-action.tsx | 2 +- .../components/running-state-action.tsx | 3 ++ .../features/accounts/components/table.tsx | 2 +- web/src/features/accounts/index.tsx | 4 ++ web/src/locales/ar.json | 2 + web/src/locales/da.json | 2 + web/src/locales/de.json | 2 + web/src/locales/en.json | 2 + web/src/locales/es.json | 2 + web/src/locales/fi.json | 2 + web/src/locales/fr.json | 2 + web/src/locales/it.json | 2 + web/src/locales/jp.json | 2 + web/src/locales/ko.json | 2 + web/src/locales/nl.json | 2 + web/src/locales/no.json | 2 + web/src/locales/pl.json | 2 + web/src/locales/pt.json | 2 + web/src/locales/ru.json | 2 + web/src/locales/sv.json | 2 + web/src/locales/zh-tw.json | 2 + web/src/locales/zh.json | 2 + 29 files changed, 106 insertions(+), 14 deletions(-) diff --git a/crates/admin/src/meta.rs b/crates/admin/src/meta.rs index f05496d..d843d62 100644 --- a/crates/admin/src/meta.rs +++ b/crates/admin/src/meta.rs @@ -262,6 +262,7 @@ impl From for AccountModel { imap_quota_bytes: None, auto_download_new_mailboxes: None, download_schedule: None, + deleting: false, } } } diff --git a/crates/core/src/account/migration.rs b/crates/core/src/account/migration.rs index 27c8bb0..170ce91 100644 --- a/crates/core/src/account/migration.rs +++ b/crates/core/src/account/migration.rs @@ -97,6 +97,8 @@ pub struct Account { pub imap_quota_window: Option, pub auto_download_new_mailboxes: Option, pub download_schedule: Option, + #[serde(default)] + pub deleting: bool, } impl MemDbModel for Account { @@ -136,6 +138,7 @@ impl Account { imap_quota_bytes: request.imap_quota_bytes, imap_quota_window: request.imap_quota_window, download_schedule: request.download_schedule, + deleting: false, }) } @@ -223,14 +226,46 @@ impl Account { pub async fn delete(account_id: u64) -> BichonResult<()> { let account = Self::get(account_id)?; - if let Err(error) = Self::cleanup_account_resources_sequential(&account).await { - tracing::error!( - "[CLEANUP_ACCOUNT_ERROR] Account {}: failed to cleanup resources: {:#?}", - account_id, - error - ); - return Err(error); + + // Immediately stop scheduling to prevent new downloads + if matches!(account.account_type, AccountType::IMAP) { + SYNC_TASKS.stop(account.id).await?; } + + // Mark as deleting and disabled so frontend shows status and download tasks skip it + update_impl( + DB_MANAGER.db(), + &account_id.to_string(), + move |current: Account| { + let mut updated = current.clone(); + updated.deleting = true; + updated.enabled = false; + Ok(updated) + }, + )?; + + // Spawn background cleanup — heavy work (Tantivy, attachments) runs off the request path + tokio::spawn(async move { + if let Err(error) = Self::cleanup_account_resources_sequential(&account).await { + tracing::error!( + "[CLEANUP_ACCOUNT_ERROR] Account {}: cleanup failed, reverting deleting flag: {:#?}", + account_id, + error + ); + // Revert deleting flag so the user can retry (only if account record still exists) + let _ = update_impl( + DB_MANAGER.db(), + &account_id.to_string(), + move |current: Account| { + let mut updated = current.clone(); + updated.deleting = false; + updated.enabled = true; + Ok(updated) + }, + ); + } + }); + Ok(()) } @@ -239,8 +274,8 @@ impl Account { } async fn cleanup_account_resources_sequential(account: &AccountModel) -> BichonResult<()> { + // Sync task already stopped in delete() before spawning this background task if matches!(account.account_type, AccountType::IMAP) { - SYNC_TASKS.stop(account.id).await?; DownloadState::delete(account.id)?; } OAuth2AccessToken::try_delete(account.id)?; diff --git a/crates/core/src/account/view.rs b/crates/core/src/account/view.rs index 36e4f6e..5703760 100644 --- a/crates/core/src/account/view.rs +++ b/crates/core/src/account/view.rs @@ -58,6 +58,7 @@ pub struct AccountResp { pub imap_quota_window: Option, pub auto_download_new_mailboxes: Option, pub download_schedule: Option, + pub deleting: bool, } impl AccountResp { @@ -95,6 +96,7 @@ impl AccountResp { imap_quota_window: account.imap_quota_window, auto_download_new_mailboxes: account.auto_download_new_mailboxes, download_schedule: account.download_schedule, + deleting: account.deleting, } } } diff --git a/crates/core/src/cache/imap/task.rs b/crates/core/src/cache/imap/task.rs index 0bac419..66659ef 100644 --- a/crates/core/src/cache/imap/task.rs +++ b/crates/core/src/cache/imap/task.rs @@ -113,6 +113,9 @@ impl AccountDownTask { let account = AccountModel::get(account_id).ok(); match account { Some(account) => { + if account.deleting { + return Ok(()); + } if !account.enabled { let last = LAST_WARN_TIME.load(Ordering::Relaxed); let now = utc_now!(); @@ -246,6 +249,10 @@ impl AccountDownTask { } }; + if account.deleting { + return; + } + if let Err(e) = process_imap_download(&account, token_clone, TriggerType::Manual).await { error!("Manual download failed for {}: {:?}", account_id, e); diff --git a/web/src/api/account/api.ts b/web/src/api/account/api.ts index cd1f4bb..5fb2218 100644 --- a/web/src/api/account/api.ts +++ b/web/src/api/account/api.ts @@ -144,6 +144,7 @@ export interface AccountModel { imap_quota_bytes?: number; auto_download_new_mailboxes?: boolean; download_schedule?: string; + deleting?: boolean; } export const download_state = async (account_id: number) => { diff --git a/web/src/features/accounts/components/data-table-row-actions.tsx b/web/src/features/accounts/components/data-table-row-actions.tsx index 5187882..6a7ff92 100644 --- a/web/src/features/accounts/components/data-table-row-actions.tsx +++ b/web/src/features/accounts/components/data-table-row-actions.tsx @@ -50,13 +50,16 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) { const hasPermission = require_any_permission(['system:root', 'account:manage'], row.original.id); const hasReadPermission = require_any_permission(['system:root', 'account:read_details'], row.original.id); + const isDeleting = row.original.deleting === true; const canShowAnyAction = + !isDeleting && ( (hasPermission) || (account_type === 'IMAP' && hasPermission) || - (account_type === 'IMAP' && hasReadPermission); + (account_type === 'IMAP' && hasReadPermission) + ); - const showDownload = account_type === 'IMAP' && hasPermission; + const showDownload = !isDeleting && account_type === 'IMAP' && hasPermission; const handleStartDownload = async () => { try { diff --git a/web/src/features/accounts/components/delete-dialog.tsx b/web/src/features/accounts/components/delete-dialog.tsx index 41e63dd..77a0880 100644 --- a/web/src/features/accounts/components/delete-dialog.tsx +++ b/web/src/features/accounts/components/delete-dialog.tsx @@ -43,8 +43,8 @@ export function AccountDeleteDialog({ open, onOpenChange, currentRow }: Props) { const queryClient = useQueryClient(); function handleSuccess() { toast({ - title: t('dialogs.accountDeleted'), - description: t('dialogs.accountDeletedDesc'), + title: t('dialogs.accountDeletionStarted'), + description: t('dialogs.accountDeletionStartedDesc'), action: {t('common.close')}, }); diff --git a/web/src/features/accounts/components/enable-action.tsx b/web/src/features/accounts/components/enable-action.tsx index e26aac2..887dfbf 100644 --- a/web/src/features/accounts/components/enable-action.tsx +++ b/web/src/features/accounts/components/enable-action.tsx @@ -77,7 +77,7 @@ export function EnableAction({ row }: DataTableRowActionsProps) { setOpen(true)} - disabled={!hasPermission || updateMutation.isPending} + disabled={!hasPermission || updateMutation.isPending || row.original.deleting} /> Deleting... + } let account_type = row.original.account_type; if (account_type === "NoSync") { return n/a diff --git a/web/src/features/accounts/components/table.tsx b/web/src/features/accounts/components/table.tsx index ef094fc..c2ba385 100644 --- a/web/src/features/accounts/components/table.tsx +++ b/web/src/features/accounts/components/table.tsx @@ -127,7 +127,7 @@ export function AccountTable({ columns, data }: DataTableProps) { {row.getVisibleCells().map((cell) => ( { + const items = (query.state.data as { items?: { deleting?: boolean }[] })?.items; + return items?.some((item) => item.deleting) ? 5000 : false; + }, }) const hasAccounts = accountList != null && accountList.items.length > 0; diff --git a/web/src/locales/ar.json b/web/src/locales/ar.json index 931cfb5..4fb65b7 100644 --- a/web/src/locales/ar.json +++ b/web/src/locales/ar.json @@ -536,6 +536,8 @@ "accountDeleteFailed": "فشل حذف الحساب", "accountDeleted": "تم حذف الحساب", "accountDeletedDesc": "تم حذف حسابك بنجاح.", + "accountDeletionStarted": "بدء حذف الحساب", + "accountDeletionStartedDesc": "جاري حذف الحساب في الخلفية، وسيختفي بعد اكتمال التنظيف.", "allResourcesErased": "سيتم مسح جميع الموارد ذات الصلة نهائيًا.", "cannotBeUndone": "لا يمكن التراجع عن هذا الإجراء!", "confirmDelete": "تأكيد الحذف", diff --git a/web/src/locales/da.json b/web/src/locales/da.json index 7fdcd08..df99490 100644 --- a/web/src/locales/da.json +++ b/web/src/locales/da.json @@ -536,6 +536,8 @@ "accountDeleteFailed": "Sletning af konto mislykkedes", "accountDeleted": "Konto slettet", "accountDeletedDesc": "Din konto er blevet slettet.", + "accountDeletionStarted": "Kontoen slettes nu", + "accountDeletionStartedDesc": "Kontoen slettes i baggrunden og forsvinder, når oprydningen er færdig.", "allResourcesErased": "Alle relaterede ressourcer vil blive slettet permanent.", "cannotBeUndone": "Denne handling kan ikke fortrydes!", "confirmDelete": "Bekræft Sletning", diff --git a/web/src/locales/de.json b/web/src/locales/de.json index 2dab0dc..179558c 100644 --- a/web/src/locales/de.json +++ b/web/src/locales/de.json @@ -536,6 +536,8 @@ "accountDeleteFailed": "Löschen des Kontos fehlgeschlagen", "accountDeleted": "Konto gelöscht", "accountDeletedDesc": "Ihr Konto wurde erfolgreich gelöscht.", + "accountDeletionStarted": "Kontolöschung gestartet", + "accountDeletionStartedDesc": "Konto wird im Hintergrund gelöscht und verschwindet nach der Bereinigung.", "allResourcesErased": "Alle zugehörigen Ressourcen werden dauerhaft gelöscht.", "cannotBeUndone": "Diese Aktion kann nicht rückgängig gemacht werden!", "confirmDelete": "Löschung bestätigen", diff --git a/web/src/locales/en.json b/web/src/locales/en.json index a4e1b50..238bcc7 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -536,6 +536,8 @@ "accountDeleteFailed": "Account delete Failed", "accountDeleted": "Account Deleted", "accountDeletedDesc": "Your account has been successfully deleted.", + "accountDeletionStarted": "Account deletion started", + "accountDeletionStartedDesc": "Account is being deleted in the background and will disappear after cleanup.", "allResourcesErased": "All related resources will be permanently erased.", "cannotBeUndone": "This action cannot be undone!", "confirmDelete": "Confirm Delete", diff --git a/web/src/locales/es.json b/web/src/locales/es.json index 47b97f4..0c4c3e9 100644 --- a/web/src/locales/es.json +++ b/web/src/locales/es.json @@ -536,6 +536,8 @@ "accountDeleteFailed": "Error al eliminar la cuenta", "accountDeleted": "Cuenta eliminada", "accountDeletedDesc": "Tu cuenta ha sido eliminada con éxito.", + "accountDeletionStarted": "Eliminación de cuenta iniciada", + "accountDeletionStartedDesc": "La cuenta se está eliminando en segundo plano y desaparecerá tras la limpieza.", "allResourcesErased": "Todos los recursos asociados se borrarán permanentemente.", "cannotBeUndone": "¡Esta acción no se puede deshacer!", "confirmDelete": "Confirmar eliminación", diff --git a/web/src/locales/fi.json b/web/src/locales/fi.json index f5e1820..9c78869 100644 --- a/web/src/locales/fi.json +++ b/web/src/locales/fi.json @@ -536,6 +536,8 @@ "accountDeleteFailed": "Tilin poistaminen epäonnistui", "accountDeleted": "Tili poistettu", "accountDeletedDesc": "Tilisi on poistettu onnistuneesti.", + "accountDeletionStarted": "Tilin poistaminen aloitettu", + "accountDeletionStartedDesc": "Tiliä poistetaan taustalla. Se katoaa, kun puhdistus on valmis.", "allResourcesErased": "Kaikki liittyvät resurssit poistetaan pysyvästi.", "cannotBeUndone": "Tätä toimenpidettä ei voi kumota!", "confirmDelete": "Vahvista poisto", diff --git a/web/src/locales/fr.json b/web/src/locales/fr.json index b553036..71f8e4d 100644 --- a/web/src/locales/fr.json +++ b/web/src/locales/fr.json @@ -536,6 +536,8 @@ "accountDeleteFailed": "Échec de la suppression du compte", "accountDeleted": "Compte Supprimé", "accountDeletedDesc": "Votre compte a été supprimé avec succès.", + "accountDeletionStarted": "Suppression du compte lancée", + "accountDeletionStartedDesc": "Compte en cours de suppression en arrière-plan, disparaîtra après nettoyage.", "allResourcesErased": "Toutes les ressources associées seront effacées définitivement.", "cannotBeUndone": "Cette action ne peut pas être annulée !", "confirmDelete": "Confirmer la Suppression", diff --git a/web/src/locales/it.json b/web/src/locales/it.json index 42b3c30..60496a9 100644 --- a/web/src/locales/it.json +++ b/web/src/locales/it.json @@ -536,6 +536,8 @@ "accountDeleteFailed": "Eliminazione account fallita", "accountDeleted": "Account Eliminato", "accountDeletedDesc": "Il tuo account è stato eliminato con successo.", + "accountDeletionStarted": "Eliminazione account avviata", + "accountDeletionStartedDesc": "L'account è in fase di eliminazione in background e scomparirà dopo la pulizia.", "allResourcesErased": "Tutte le risorse correlate verranno cancellate permanentemente.", "cannotBeUndone": "Questa azione non può essere annullata!", "confirmDelete": "Conferma Eliminazione", diff --git a/web/src/locales/jp.json b/web/src/locales/jp.json index 438ddcd..59fe40b 100644 --- a/web/src/locales/jp.json +++ b/web/src/locales/jp.json @@ -536,6 +536,8 @@ "accountDeleteFailed": "アカウントの削除に失敗しました", "accountDeleted": "アカウントが削除されました", "accountDeletedDesc": "アカウントが正常に削除されました。", + "accountDeletionStarted": "アカウントの削除を開始しました", + "accountDeletionStartedDesc": "バックグラウンドで削除中です。完了するとリストから消えます。", "allResourcesErased": "関連するすべてのリソースは完全に消去されます。", "cannotBeUndone": "この操作は元に戻せません!", "confirmDelete": "削除の確認", diff --git a/web/src/locales/ko.json b/web/src/locales/ko.json index 77e17ff..6da8d17 100644 --- a/web/src/locales/ko.json +++ b/web/src/locales/ko.json @@ -536,6 +536,8 @@ "accountDeleteFailed": "계정 삭제 실패", "accountDeleted": "계정 삭제됨", "accountDeletedDesc": "계정이 성공적으로 삭제되었습니다.", + "accountDeletionStarted": "계정 삭제 시작됨", + "accountDeletionStartedDesc": "백그라운드에서 삭제 중이며, 정리가 끝나면 목록에서 사라집니다.", "allResourcesErased": "모든 관련 리소스가 영구적으로 지워집니다.", "cannotBeUndone": "이 작업은 되돌릴 수 없습니다!", "confirmDelete": "삭제 확인", diff --git a/web/src/locales/nl.json b/web/src/locales/nl.json index 396e2e1..85d4066 100644 --- a/web/src/locales/nl.json +++ b/web/src/locales/nl.json @@ -536,6 +536,8 @@ "accountDeleteFailed": "Account verwijderen Mislukt", "accountDeleted": "Account Verwijderd", "accountDeletedDesc": "Uw account is succesvol verwijderd.", + "accountDeletionStarted": "Verwijdering account gestart", + "accountDeletionStartedDesc": "Account wordt op de achtergrond verwijderd en verdwijnt na opschonen.", "allResourcesErased": "Alle gerelateerde bronnen worden permanent gewist.", "cannotBeUndone": "Deze actie kan niet ongedaan worden gemaakt!", "confirmDelete": "Verwijdering Bevestigen", diff --git a/web/src/locales/no.json b/web/src/locales/no.json index 6e7d619..0c21ca2 100644 --- a/web/src/locales/no.json +++ b/web/src/locales/no.json @@ -536,6 +536,8 @@ "accountDeleteFailed": "Sletting av konto mislyktes", "accountDeleted": "Konto slettet", "accountDeletedDesc": "Kontoen din har blitt slettet.", + "accountDeletionStarted": "Kontosletting startet", + "accountDeletionStartedDesc": "Kontoen slettes i bakgrunnen og forsvinner når opprydningen er ferdig.", "allResourcesErased": "Alle relaterte ressurser vil bli permanent slettet.", "cannotBeUndone": "Denne handlingen kan ikke angres!", "confirmDelete": "Bekreft sletting", diff --git a/web/src/locales/pl.json b/web/src/locales/pl.json index 65999c9..4445656 100644 --- a/web/src/locales/pl.json +++ b/web/src/locales/pl.json @@ -536,6 +536,8 @@ "accountDeleteFailed": "Bład podczas usuwania konta", "accountDeleted": "Konto usunięte", "accountDeletedDesc": "Konto zostało usunięte.", + "accountDeletionStarted": "Rozpoczęto usuwanie konta", + "accountDeletionStartedDesc": "Konto jest usuwane w tle i zniknie po zakończeniu czyszczenia.", "allResourcesErased": "Wszystkie powiązane zasoby zostaną trwale usunięte.", "cannotBeUndone": "Tej czynności nie można cofnąć!", "confirmDelete": "Potwierdź usunięcie", diff --git a/web/src/locales/pt.json b/web/src/locales/pt.json index c47dac7..51ce366 100644 --- a/web/src/locales/pt.json +++ b/web/src/locales/pt.json @@ -536,6 +536,8 @@ "accountDeleteFailed": "Falha ao Excluir Conta", "accountDeleted": "Conta Excluída", "accountDeletedDesc": "A conta foi excluída com sucesso.", + "accountDeletionStarted": "Exclusão da conta iniciada", + "accountDeletionStartedDesc": "A conta está sendo excluída em segundo plano e desaparecerá após a limpeza.", "allResourcesErased": "Todos os recursos relacionados serão permanentemente apagados.", "cannotBeUndone": "Esta ação não pode ser desfeita!", "confirmDelete": "Confirmar Exclusão", diff --git a/web/src/locales/ru.json b/web/src/locales/ru.json index d22f2af..f18284a 100644 --- a/web/src/locales/ru.json +++ b/web/src/locales/ru.json @@ -536,6 +536,8 @@ "accountDeleteFailed": "Ошибка удаления аккаунта", "accountDeleted": "Аккаунт удален", "accountDeletedDesc": "Ваш аккаунт был успешно удален.", + "accountDeletionStarted": "Удаление аккаунта запущено", + "accountDeletionStartedDesc": "Аккаунт удаляется в фоновом режиме и исчезнет после очистки.", "allResourcesErased": "Все связанные ресурсы будут безвозвратно стерты.", "cannotBeUndone": "Это действие нельзя отменить!", "confirmDelete": "Подтвердить удаление", diff --git a/web/src/locales/sv.json b/web/src/locales/sv.json index 7a70218..a021acf 100644 --- a/web/src/locales/sv.json +++ b/web/src/locales/sv.json @@ -536,6 +536,8 @@ "accountDeleteFailed": "Borttagning av konto misslyckades", "accountDeleted": "Konto raderat", "accountDeletedDesc": "Ditt konto har tagits bort.", + "accountDeletionStarted": "Kontoradering har startat", + "accountDeletionStartedDesc": "Kontot raderas i bakgrunden och försvinner när rensningen är klar.", "allResourcesErased": "Alla relaterade resurser kommer att raderas permanent.", "cannotBeUndone": "Denna åtgärd kan inte ångras!", "confirmDelete": "Bekräfta borttagning", diff --git a/web/src/locales/zh-tw.json b/web/src/locales/zh-tw.json index db11b24..a348b89 100644 --- a/web/src/locales/zh-tw.json +++ b/web/src/locales/zh-tw.json @@ -536,6 +536,8 @@ "accountDeleteFailed": "帳號刪除失敗", "accountDeleted": "帳號已刪除", "accountDeletedDesc": "帳號已成功刪除。", + "accountDeletionStarted": "帳戶刪除已開始", + "accountDeletionStartedDesc": "帳戶正在背景刪除,清理完成後將從列表中消失。", "allResourcesErased": "所有相關資源將被永久清除。", "cannotBeUndone": "此操作無法復原!", "confirmDelete": "確認刪除", diff --git a/web/src/locales/zh.json b/web/src/locales/zh.json index e3045eb..4d23a90 100644 --- a/web/src/locales/zh.json +++ b/web/src/locales/zh.json @@ -536,6 +536,8 @@ "accountDeleteFailed": "账户删除失败", "accountDeleted": "账户已删除", "accountDeletedDesc": "您的账户已成功删除。", + "accountDeletionStarted": "账户删除已开始", + "accountDeletionStartedDesc": "账户正在后台删除,清理完成后将从列表中消失。", "allResourcesErased": "所有相关资源将被永久删除。", "cannotBeUndone": "此操作无法撤销!", "confirmDelete": "确认删除",