From 1cfc12324fc6895c87c8d6b4bdfa3516c70f69e6 Mon Sep 17 00:00:00 2001 From: rustmailer Date: Sat, 29 Nov 2025 11:41:13 +0800 Subject: [PATCH] fix(ui): Handle IMAP connection failure gracefully during folder sync #23 --- src/modules/imap/client.rs | 4 +- .../accounts/components/sync-folders.tsx | 64 +++++++++++++++---- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/modules/imap/client.rs b/src/modules/imap/client.rs index 1e91589..a5c2188 100644 --- a/src/modules/imap/client.rs +++ b/src/modules/imap/client.rs @@ -144,7 +144,7 @@ impl Client { .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))? .ok_or_else(|| { raise_error!( - "failed to read greeting".into(), + "Failed to read IMAP greeting — this usually indicates an incorrect encryption setting (SSL vs. STARTTLS). Your current setting is SSL.".into(), ErrorCode::ImapCommandFailed ) })?; @@ -205,7 +205,7 @@ impl Client { .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))? .ok_or_else(|| { raise_error!( - "failed to read greeting".into(), + "Failed to read IMAP greeting — this usually indicates an incorrect encryption setting (SSL vs. STARTTLS). Your current setting is STARTTLS.".into(), ErrorCode::ImapCommandFailed ) })?; diff --git a/web/src/features/accounts/components/sync-folders.tsx b/web/src/features/accounts/components/sync-folders.tsx index 4f49067..5bf4071 100644 --- a/web/src/features/accounts/components/sync-folders.tsx +++ b/web/src/features/accounts/components/sync-folders.tsx @@ -26,18 +26,18 @@ import { DialogFooter, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { Loader2, CheckSquare, Square } from 'lucide-react' -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { AccountModel } from '../data/schema' import { toast } from '@/hooks/use-toast' -import { list_mailboxes } from '@/api/mailbox/api' +import { list_mailboxes, MailboxData } from '@/api/mailbox/api' import { buildTree } from '@/lib/build-tree' import { TreeDataItem, TreeView } from '@/components/tree-view' import { Skeleton } from '@/components/ui/skeleton' import { update_account } from '@/api/account/api' import { ToastAction } from '@/components/ui/toast' -import { AxiosError } from 'axios' +import axios, { AxiosError } from 'axios' import { ScrollArea } from '@/components/ui/scroll-area' import { useTranslation } from 'react-i18next' @@ -50,13 +50,48 @@ interface Props { export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) { const [selectedFolders, setSelectedFolders] = useState(currentRow.sync_folders || []); const [isSubmitting, setIsSubmitting] = useState(false); + + const [mailboxes, setMailboxes] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); const queryClient = useQueryClient(); const { t } = useTranslation() - const { data: mailboxes, isLoading } = useQuery({ - queryKey: ['account-mailboxes', currentRow.id], - queryFn: () => list_mailboxes(currentRow.id, true), - enabled: open, - }); + + + useEffect(() => { + if (!open) return; + let cancelled = false; + const fetchMailboxes = async () => { + setIsLoading(true); + try { + const data = await list_mailboxes(currentRow.id, true); + if (!cancelled) { + setMailboxes(data); + setError(undefined); + } + } catch (err: any) { + if (axios.isAxiosError(err)) { + const resData = err.response?.data; + if (resData) { + setError(`Error ${resData.code || ''}: ${resData.message || ''}`); + } else { + setError(err.message); + } + } else { + console.error('Other error:', err); + } + if (!cancelled) { + setMailboxes([]); + } + } finally { + if (!cancelled) setIsLoading(false); + } + }; + fetchMailboxes(); + return () => { + cancelled = true; + }; + }, [currentRow, open]); // Convert mailbox names to IDs for initial selection const initialSelectedItemIds = useMemo(() => { @@ -228,6 +263,11 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) { onSelectItemsChange={handleSelectItems} /> )} + {error && ( +
+ {error} +
+ )} @@ -237,14 +277,14 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) { onClick={() => onOpenChange(false)} disabled={isSubmitting} > - Cancel + {t('common.cancel')}