fix(ui): Handle IMAP connection failure gracefully during folder sync #23

This commit is contained in:
rustmailer
2025-11-29 11:42:11 +08:00
parent dffdac3eb6
commit 1cfc12324f
2 changed files with 54 additions and 14 deletions
+2 -2
View File
@@ -144,7 +144,7 @@ impl Client {
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))? .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?
.ok_or_else(|| { .ok_or_else(|| {
raise_error!( 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 ErrorCode::ImapCommandFailed
) )
})?; })?;
@@ -205,7 +205,7 @@ impl Client {
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))? .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?
.ok_or_else(|| { .ok_or_else(|| {
raise_error!( 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 ErrorCode::ImapCommandFailed
) )
})?; })?;
@@ -26,18 +26,18 @@ import {
DialogFooter, DialogFooter,
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button' 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 { 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 { AccountModel } from '../data/schema'
import { toast } from '@/hooks/use-toast' 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 { buildTree } from '@/lib/build-tree'
import { TreeDataItem, TreeView } from '@/components/tree-view' import { TreeDataItem, TreeView } from '@/components/tree-view'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { update_account } from '@/api/account/api' import { update_account } from '@/api/account/api'
import { ToastAction } from '@/components/ui/toast' import { ToastAction } from '@/components/ui/toast'
import { AxiosError } from 'axios' import axios, { AxiosError } from 'axios'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -50,13 +50,48 @@ interface Props {
export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) { export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
const [selectedFolders, setSelectedFolders] = useState<string[]>(currentRow.sync_folders || []); const [selectedFolders, setSelectedFolders] = useState<string[]>(currentRow.sync_folders || []);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [mailboxes, setMailboxes] = useState<MailboxData[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { t } = useTranslation() const { t } = useTranslation()
const { data: mailboxes, isLoading } = useQuery({
queryKey: ['account-mailboxes', currentRow.id],
queryFn: () => list_mailboxes(currentRow.id, true), useEffect(() => {
enabled: open, 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 // Convert mailbox names to IDs for initial selection
const initialSelectedItemIds = useMemo(() => { const initialSelectedItemIds = useMemo(() => {
@@ -228,6 +263,11 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
onSelectItemsChange={handleSelectItems} onSelectItemsChange={handleSelectItems}
/> />
)} )}
{error && (
<div className="mt-auto p-2 text-red-600 text-sm font-medium">
{error}
</div>
)}
</ScrollArea> </ScrollArea>
</div> </div>
@@ -237,14 +277,14 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
disabled={isSubmitting} disabled={isSubmitting}
> >
Cancel {t('common.cancel')}
</Button> </Button>
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
disabled={isSubmitting || isLoading} disabled={isSubmitting || isLoading || !!error}
> >
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save Changes {t('common.save')}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>