fix: IMAP-Konto bearbeiten + Löschen auch bei sync_running
- Store: UpdateCredentials() — Zugangsdaten + Passwort neu verschlüsseln,
setzt status='idle', error_msg='', sync_running=false zurück
- Handler: PATCH /api/imap/{id} unterstützt nun Credential-Update
(name/host/username vorhanden = Credential-Update, sonst sync_interval)
- Frontend: "Bearbeiten"-Button öffnet Edit-Dialog mit allen Feldern;
Passwort-Feld leer = unverändertes Passwort
- Frontend: Löschen-Button nicht mehr durch sync_running blockiert
(nur noch bei status=running gesperrt)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -273,7 +273,9 @@ func (s *Server) handleSyncNow(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, acc)
|
||||
}
|
||||
|
||||
// handleUpdateImapInterval updates the automatic sync interval for an IMAP account.
|
||||
// handleUpdateImapInterval updates the automatic sync interval or full credentials
|
||||
// of an IMAP account. When name/host/username are present the credentials are updated;
|
||||
// otherwise only sync_interval_min is changed.
|
||||
func (s *Server) handleUpdateImapInterval(w http.ResponseWriter, r *http.Request) {
|
||||
if s.imapStore == nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "IMAP not configured")
|
||||
@@ -299,13 +301,48 @@ func (s *Server) handleUpdateImapInterval(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
var req struct {
|
||||
SyncIntervalMin int `json:"sync_interval_min"`
|
||||
SyncIntervalMin int `json:"sync_interval_min"`
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
TLS string `json:"tls"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
// Credential update when name/host/username are provided.
|
||||
if req.Name != "" || req.Host != "" || req.Username != "" {
|
||||
if req.Name == "" || req.Host == "" || req.Username == "" {
|
||||
writeError(w, http.StatusBadRequest, "name, host and username are required for credential update")
|
||||
return
|
||||
}
|
||||
port := req.Port
|
||||
if port == 0 {
|
||||
port = acc.Port
|
||||
}
|
||||
tls := req.TLS
|
||||
if tls == "" {
|
||||
tls = acc.TLS
|
||||
}
|
||||
updated := imapstore.Account{Name: req.Name, Host: req.Host, Port: port, TLS: tls, Username: req.Username}
|
||||
if err := s.imapStore.UpdateCredentials(r.Context(), id, updated, req.Password); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to update credentials")
|
||||
return
|
||||
}
|
||||
acc, err = s.imapStore.Get(r.Context(), id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to reload account")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, acc)
|
||||
return
|
||||
}
|
||||
|
||||
// Sync interval update.
|
||||
// 0 = disabled; otherwise must be between 5 and 1440 minutes.
|
||||
if req.SyncIntervalMin != 0 && (req.SyncIntervalMin < 5 || req.SyncIntervalMin > 1440) {
|
||||
writeError(w, http.StatusBadRequest, "sync_interval_min must be 0 (disabled) or between 5 and 1440")
|
||||
|
||||
@@ -282,6 +282,33 @@ func (s *Store) UpdateDone(ctx context.Context, id int64, count int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCredentials updates the connection details and optionally the password
|
||||
// of an IMAP account. Pass an empty password to leave it unchanged.
|
||||
func (s *Store) UpdateCredentials(ctx context.Context, id int64, acc Account, password string) error {
|
||||
if password != "" {
|
||||
enc, err := encryptPassword(password, s.encKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("imap store: encrypt password: %w", err)
|
||||
}
|
||||
_, err = s.pool.Exec(ctx,
|
||||
`UPDATE imap_accounts SET name=$1, host=$2, port=$3, tls=$4, username=$5, password_enc=$6,
|
||||
status='idle', error_msg='', sync_running=false WHERE id=$7`,
|
||||
acc.Name, acc.Host, acc.Port, acc.TLS, acc.Username, enc, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("imap store: update credentials: %w", err)
|
||||
}
|
||||
} else {
|
||||
_, err := s.pool.Exec(ctx,
|
||||
`UPDATE imap_accounts SET name=$1, host=$2, port=$3, tls=$4, username=$5,
|
||||
status='idle', error_msg='', sync_running=false WHERE id=$6`,
|
||||
acc.Name, acc.Host, acc.Port, acc.TLS, acc.Username, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("imap store: update credentials (no pw): %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSyncInterval sets the automatic sync interval for an account.
|
||||
// intervalMin == 0 disables automatic sync.
|
||||
func (s *Store) UpdateSyncInterval(ctx context.Context, id int64, intervalMin int) error {
|
||||
|
||||
+108
-1
@@ -12,6 +12,7 @@ import {
|
||||
getImapProgress,
|
||||
triggerImapSync,
|
||||
updateImapInterval,
|
||||
updateImapAccount,
|
||||
type ImapAccount,
|
||||
type ImapFolder,
|
||||
} from "@/lib/api";
|
||||
@@ -50,6 +51,17 @@ export default function ImapPage() {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<number | null>(null);
|
||||
|
||||
// Edit state
|
||||
const [editAccount, setEditAccount] = useState<ImapAccount | null>(null);
|
||||
const [editName, setEditName] = useState("");
|
||||
const [editHost, setEditHost] = useState("");
|
||||
const [editPort, setEditPort] = useState("993");
|
||||
const [editTls, setEditTls] = useState("ssl");
|
||||
const [editUsername, setEditUsername] = useState("");
|
||||
const [editPassword, setEditPassword] = useState("");
|
||||
const [editSaving, setEditSaving] = useState(false);
|
||||
const [editError, setEditError] = useState("");
|
||||
|
||||
// Form state
|
||||
const [formName, setFormName] = useState("");
|
||||
const [formHost, setFormHost] = useState("");
|
||||
@@ -209,6 +221,43 @@ export default function ImapPage() {
|
||||
setDeleteConfirm(null);
|
||||
}
|
||||
|
||||
function openEdit(acc: ImapAccount) {
|
||||
setEditAccount(acc);
|
||||
setEditName(acc.name);
|
||||
setEditHost(acc.host);
|
||||
setEditPort(String(acc.port));
|
||||
setEditTls(acc.tls);
|
||||
setEditUsername(acc.username);
|
||||
setEditPassword("");
|
||||
setEditError("");
|
||||
}
|
||||
|
||||
async function handleEditSave() {
|
||||
if (!editAccount) return;
|
||||
if (!editName || !editHost || !editUsername) {
|
||||
setEditError("Name, Host und Benutzername sind Pflichtfelder.");
|
||||
return;
|
||||
}
|
||||
setEditSaving(true);
|
||||
setEditError("");
|
||||
try {
|
||||
const updated = await updateImapAccount(editAccount.id, {
|
||||
name: editName,
|
||||
host: editHost,
|
||||
port: parseInt(editPort, 10),
|
||||
tls: editTls,
|
||||
username: editUsername,
|
||||
password: editPassword || undefined,
|
||||
});
|
||||
setAccounts((prev) => prev.map((a) => (a.id === updated.id ? updated : a)));
|
||||
setEditAccount(null);
|
||||
} catch (err) {
|
||||
setEditError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setEditSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSyncNow(id: number) {
|
||||
try {
|
||||
const updated = await triggerImapSync(id);
|
||||
@@ -407,10 +456,17 @@ export default function ImapPage() {
|
||||
>
|
||||
Sync jetzt
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => openEdit(acc)}
|
||||
>
|
||||
Bearbeiten
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
disabled={acc.status === "running" || acc.sync_running}
|
||||
disabled={acc.status === "running"}
|
||||
onClick={() => setDeleteConfirm(acc.id)}
|
||||
>
|
||||
Loeschen
|
||||
@@ -572,6 +628,57 @@ export default function ImapPage() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Edit Account Dialog */}
|
||||
<Dialog open={editAccount !== null} onOpenChange={(open) => { if (!open) setEditAccount(null); }}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>IMAP-Konto bearbeiten</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label>Name</Label>
|
||||
<Input value={editName} onChange={(e) => setEditName(e.target.value)} placeholder="z.B. Firmen-Mail" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label>Host</Label>
|
||||
<Input value={editHost} onChange={(e) => setEditHost(e.target.value)} placeholder="imap.example.com" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>Port</Label>
|
||||
<Input value={editPort} onChange={(e) => setEditPort(e.target.value)} type="number" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>TLS</Label>
|
||||
<Select value={editTls} onValueChange={setEditTls}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ssl">SSL/TLS</SelectItem>
|
||||
<SelectItem value="starttls">STARTTLS</SelectItem>
|
||||
<SelectItem value="none">Keine</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>Benutzername</Label>
|
||||
<Input value={editUsername} onChange={(e) => setEditUsername(e.target.value)} placeholder="user@example.com" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>Passwort <span className="text-muted-foreground text-xs">(leer lassen = unveraendert)</span></Label>
|
||||
<Input value={editPassword} onChange={(e) => setEditPassword(e.target.value)} type="password" placeholder="Neues Passwort eingeben" />
|
||||
</div>
|
||||
{editError && <p className="text-sm text-destructive">{editError}</p>}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setEditAccount(null)}>Abbrechen</Button>
|
||||
<Button onClick={handleEditSave} disabled={editSaving}>
|
||||
{editSaving ? "Speichert..." : "Speichern"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog
|
||||
open={deleteConfirm !== null}
|
||||
|
||||
@@ -392,6 +392,16 @@ export async function updateImapInterval(id: number, intervalMin: number): Promi
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateImapAccount(
|
||||
id: number,
|
||||
data: { name: string; host: string; port: number; tls: string; username: string; password?: string }
|
||||
): Promise<ImapAccount> {
|
||||
return request<ImapAccount>(`/api/imap/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
// ── POP3 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface Pop3Account {
|
||||
|
||||
Reference in New Issue
Block a user