From c59cad92be20772030d90b255951fc42ea11aa22 Mon Sep 17 00:00:00 2001 From: sysops Date: Fri, 20 Mar 2026 00:49:37 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20IMAP-Konto=20bearbeiten=20+=20L=C3=B6sch?= =?UTF-8?q?en=20auch=20bei=20sync=5Frunning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- internal/api/import_handlers.go | 41 +++++++++++- internal/imap/store.go | 27 ++++++++ src/app/imap/page.tsx | 109 +++++++++++++++++++++++++++++++- src/lib/api.ts | 10 +++ 4 files changed, 184 insertions(+), 3 deletions(-) diff --git a/internal/api/import_handlers.go b/internal/api/import_handlers.go index c9d1343..ae08d67 100644 --- a/internal/api/import_handlers.go +++ b/internal/api/import_handlers.go @@ -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") diff --git a/internal/imap/store.go b/internal/imap/store.go index 1b576cb..5f169d2 100644 --- a/internal/imap/store.go +++ b/internal/imap/store.go @@ -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 { diff --git a/src/app/imap/page.tsx b/src/app/imap/page.tsx index f3467c5..d3bb521 100644 --- a/src/app/imap/page.tsx +++ b/src/app/imap/page.tsx @@ -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(null); + // Edit state + const [editAccount, setEditAccount] = useState(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 + + + + + + {/* Delete Confirmation Dialog */} { + return request(`/api/imap/${id}`, { + method: "PATCH", + body: JSON.stringify(data), + }); +} + // ── POP3 ────────────────────────────────────────────────────────────────── export interface Pop3Account {