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.
175 lines
6.9 KiB
TypeScript
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>
|
|
)
|
|
}
|