Files
mail-client/src/components/ImportModal.tsx
T
KanjiG 20bfd2cf6b Initial commit: Outlook-competitor desktop mail client
Tauri v2 (Rust backend, React/TypeScript frontend). IMAP/POP3/SMTP core,
Google OAuth (mail/calendar/contacts sync), CalDAV, local calendar with
recurrence, rules engine, contacts, tasks, unified inbox, context menus,
drag & drop, snooze, templates, and more.
2026-07-28 20:56:05 +02:00

175 lines
6.9 KiB
TypeScript

import { useEffect, useState } from 'react'
import { open } from '@tauri-apps/plugin-dialog'
import { HardDrive, Inbox, Upload, X } from 'lucide-react'
import type { Account, Folder } from '../types'
import { api } from '../lib/api'
const inputClass =
'rounded-lg border border-zinc-200 bg-white px-3 py-1.5 text-[13px] text-zinc-800 outline-none transition focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100 dark:focus:ring-indigo-500/20'
export function ImportModal({
open: isOpen,
accounts,
onClose,
onImported,
}: {
open: boolean
accounts: Account[]
onClose: () => void
onImported: (accountId: string, folder: string) => void
}) {
const targets = accounts.filter((a) => a.protocol === 'local' || a.protocol === 'imap')
const [accountId, setAccountId] = useState('')
const [folders, setFolders] = useState<Folder[]>([])
const [folder, setFolder] = useState('')
const [loadingFolders, setLoadingFolders] = useState(false)
const [importing, setImporting] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (isOpen) {
const first = targets[0]?.id ?? ''
setAccountId(first)
setError(null)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen])
useEffect(() => {
if (!accountId) return
const account = accounts.find((a) => a.id === accountId)
if (!account) return
if (account.protocol === 'local') {
setFolders([{ name: 'Importiert', display_name: 'Importiert', unread: 0 }])
setFolder('Importiert')
return
}
setLoadingFolders(true)
api
.listFolders(accountId)
.then((f) => {
setFolders(f)
setFolder(f[0]?.name ?? '')
})
.catch((e) => setError(String(e)))
.finally(() => setLoadingFolders(false))
}, [accountId, accounts])
if (!isOpen) return null
async function handlePickAndImport() {
if (!accountId || !folder) return
setError(null)
const path = await open({
multiple: false,
filters: [{ name: 'E-Mail', extensions: ['eml', 'mbox', 'mbx'] }],
})
if (!path || Array.isArray(path)) return
setImporting(true)
try {
if (path.toLowerCase().endsWith('.eml')) {
await api.importEmlFile(accountId, folder, path)
} else {
await api.importMboxFile(accountId, folder, path)
}
onImported(accountId, folder)
onClose()
} catch (e) {
setError(String(e))
} finally {
setImporting(false)
}
}
return (
<div className="animate-overlay-in fixed inset-0 z-50 flex items-center justify-center bg-zinc-900/40 p-6 backdrop-blur-sm dark:bg-black/60">
<div className="animate-panel-in flex w-full max-w-sm flex-col overflow-hidden rounded-[28px] border border-white/70 bg-white/95 shadow-[0_24px_70px_-12px_rgba(15,15,25,0.35)] ring-1 ring-black/[0.03] backdrop-blur-2xl dark:border-white/10 dark:bg-zinc-900/90 dark:shadow-[0_24px_70px_-12px_rgba(0,0,0,0.6)]">
<div className="flex items-center justify-between border-b border-zinc-200/70 px-6 py-4 dark:border-zinc-800/70">
<h2 className="text-[15px] font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
Mail importieren
</h2>
<button
onClick={onClose}
className="rounded-full p-1.5 text-zinc-400 transition hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800"
>
<X size={16} />
</button>
</div>
<div className="space-y-3.5 px-6 py-5">
<p className="text-[12.5px] text-zinc-500 dark:text-zinc-400">
Unterstützt .eml (einzelne Nachricht) und .mbox (mehrere Nachrichten). Wähle zuerst, wohin importiert
werden soll.
</p>
<label className="flex flex-col gap-1">
<span className="text-[11.5px] font-medium text-zinc-500 dark:text-zinc-400">Ziel</span>
<select className={inputClass} value={accountId} onChange={(e) => setAccountId(e.target.value)}>
{targets.map((a) => (
<option key={a.id} value={a.id}>
{a.protocol === 'local' ? 'Lokal (nur in dieser App)' : `${a.label} (${a.email})`}
</option>
))}
</select>
</label>
{accounts.find((a) => a.id === accountId)?.protocol === 'imap' && (
<label className="flex flex-col gap-1">
<span className="text-[11.5px] font-medium text-zinc-500 dark:text-zinc-400">Zielordner</span>
<select
className={inputClass}
value={folder}
onChange={(e) => setFolder(e.target.value)}
disabled={loadingFolders}
>
{folders.map((f) => (
<option key={f.name} value={f.name}>
{f.display_name}
</option>
))}
</select>
</label>
)}
{accounts.find((a) => a.id === accountId)?.protocol === 'local' && (
<div className="flex items-center gap-2 rounded-lg bg-zinc-50 px-3 py-2 text-[12px] text-zinc-500 dark:bg-zinc-800/60 dark:text-zinc-400">
<HardDrive size={13} className="shrink-0" />
Nachrichten werden nur lokal gespeichert, nicht auf einen Server hochgeladen.
</div>
)}
{accounts.find((a) => a.id === accountId)?.protocol === 'imap' && (
<div className="flex items-center gap-2 rounded-lg bg-zinc-50 px-3 py-2 text-[12px] text-zinc-500 dark:bg-zinc-800/60 dark:text-zinc-400">
<Inbox size={13} className="shrink-0" />
Nachrichten werden per IMAP wirklich in dieses Postfach hochgeladen.
</div>
)}
{error && (
<p className="rounded-lg bg-red-50 px-3 py-2 text-[12px] text-red-600 dark:bg-red-950/40 dark:text-red-400">
{error}
</p>
)}
</div>
<div className="flex justify-end gap-2 border-t border-zinc-200/70 px-6 py-4 dark:border-zinc-800/70">
<button
onClick={onClose}
className="rounded-full px-4 py-2 text-[13px] font-medium text-zinc-500 transition hover:bg-zinc-100 dark:hover:bg-zinc-800"
>
Abbrechen
</button>
<button
onClick={handlePickAndImport}
disabled={importing || !accountId || !folder}
className="flex items-center gap-2 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 px-4 py-2 text-[13px] font-medium text-white shadow-lg shadow-indigo-600/25 transition hover:from-indigo-400 hover:to-indigo-500 active:scale-[0.98] disabled:opacity-60"
>
<Upload size={14} />
{importing ? 'Importiere…' : 'Datei wählen…'}
</button>
</div>
</div>
</div>
)
}