feat(PROJ-8): Automatischer IMAP-Sync (Cron-Scheduler)
Backend:
- internal/imap/store.go: 7 neue Felder (sync_interval_min, last_sync_at,
last_sync_count, last_uid, sync_running, sync_status, sync_error_msg)
DB-Migration via ALTER TABLE ADD COLUMN IF NOT EXISTS
Neue Methoden: ListAll, UpdateSyncInterval, SetSyncRunning, UpdateSyncResult
- internal/imap/scheduler.go: Scheduler mit time.Ticker (1 min),
inkrementeller Sync via UID SEARCH UID <lastUID+1>:*,
exponential backoff (3 Versuche: 1s / 60s / 300s),
sync_running-Flag verhindert parallele Syncs
- internal/api/server.go: POST /api/imap/{id}/sync (manueller Trigger),
PATCH /api/imap/{id} (sync_interval_min setzen, 0 oder 5-1440 min)
- cmd/archivmail/main.go: Scheduler gestartet + via SetImap verdrahtet
Frontend:
- src/lib/api.ts: 6 neue ImapAccount-Felder, triggerImapSync, updateImapInterval
- src/app/imap/page.tsx: Intervall-Dropdown, "Sync jetzt"-Button,
Letzter-Sync-Anzeige mit Status-Badge, Polling auch bei sync_running
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+92
-5
@@ -41,9 +41,10 @@ type Server struct {
|
||||
audlog *audit.Logger
|
||||
logger *slog.Logger
|
||||
mux *http.ServeMux
|
||||
smtpDaemon *smtpd.Daemon
|
||||
imapStore *imapstore.Store
|
||||
imapImporter *imapstore.Importer
|
||||
smtpDaemon *smtpd.Daemon
|
||||
imapStore *imapstore.Store
|
||||
imapImporter *imapstore.Importer
|
||||
imapScheduler *imapstore.Scheduler
|
||||
}
|
||||
|
||||
// SetSMTPDaemon wires the SMTP daemon into the API server after construction.
|
||||
@@ -51,10 +52,11 @@ func (s *Server) SetSMTPDaemon(d *smtpd.Daemon) {
|
||||
s.smtpDaemon = d
|
||||
}
|
||||
|
||||
// SetImap wires the IMAP store and importer into the API server after construction.
|
||||
func (s *Server) SetImap(store *imapstore.Store, importer *imapstore.Importer) {
|
||||
// SetImap wires the IMAP store, importer, and scheduler into the API server after construction.
|
||||
func (s *Server) SetImap(store *imapstore.Store, importer *imapstore.Importer, scheduler *imapstore.Scheduler) {
|
||||
s.imapStore = store
|
||||
s.imapImporter = importer
|
||||
s.imapScheduler = scheduler
|
||||
}
|
||||
|
||||
// New creates and wires up a new API server.
|
||||
@@ -110,9 +112,11 @@ func (s *Server) routes() {
|
||||
s.mux.HandleFunc("GET /api/imap", s.authMiddleware(s.handleListImap))
|
||||
s.mux.HandleFunc("POST /api/imap", s.authMiddleware(s.handleCreateImap))
|
||||
s.mux.HandleFunc("DELETE /api/imap/{id}", s.authMiddleware(s.handleDeleteImap))
|
||||
s.mux.HandleFunc("PATCH /api/imap/{id}", s.authMiddleware(s.handleUpdateImapInterval))
|
||||
s.mux.HandleFunc("POST /api/imap/test", s.authMiddleware(s.handleTestImap))
|
||||
s.mux.HandleFunc("POST /api/imap/{id}/import", s.authMiddleware(s.handleStartImport))
|
||||
s.mux.HandleFunc("GET /api/imap/{id}/progress", s.authMiddleware(s.handleImapProgress))
|
||||
s.mux.HandleFunc("POST /api/imap/{id}/sync", s.authMiddleware(s.handleSyncNow))
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler.
|
||||
@@ -1205,6 +1209,89 @@ func (s *Server) handleImapProgress(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, acc)
|
||||
}
|
||||
|
||||
// handleSyncNow triggers an immediate incremental sync for a single IMAP account.
|
||||
func (s *Server) handleSyncNow(w http.ResponseWriter, r *http.Request) {
|
||||
if s.imapStore == nil || s.imapScheduler == nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "IMAP not configured")
|
||||
return
|
||||
}
|
||||
idStr := r.PathValue("id")
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
acc, err := s.imapStore.Get(r.Context(), id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "account not found")
|
||||
return
|
||||
}
|
||||
|
||||
sess := sessionFromCtx(r.Context())
|
||||
if acc.Owner != sess.Username && sess.Role != userstore.RoleAdmin {
|
||||
writeError(w, http.StatusForbidden, "access denied")
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.imapScheduler.TriggerSync(r.Context(), id); err != nil {
|
||||
writeError(w, http.StatusConflict, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Return the account with the updated sync_running flag reflected immediately.
|
||||
acc.SyncRunning = true
|
||||
writeJSON(w, http.StatusOK, acc)
|
||||
}
|
||||
|
||||
// handleUpdateImapInterval updates the automatic sync interval for an IMAP account.
|
||||
func (s *Server) handleUpdateImapInterval(w http.ResponseWriter, r *http.Request) {
|
||||
if s.imapStore == nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "IMAP not configured")
|
||||
return
|
||||
}
|
||||
idStr := r.PathValue("id")
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
acc, err := s.imapStore.Get(r.Context(), id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "account not found")
|
||||
return
|
||||
}
|
||||
|
||||
sess := sessionFromCtx(r.Context())
|
||||
if acc.Owner != sess.Username && sess.Role != userstore.RoleAdmin {
|
||||
writeError(w, http.StatusForbidden, "access denied")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
SyncIntervalMin int `json:"sync_interval_min"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
// 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")
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.imapStore.UpdateSyncInterval(r.Context(), id, req.SyncIntervalMin); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to update sync interval")
|
||||
return
|
||||
}
|
||||
|
||||
acc.SyncIntervalMin = req.SyncIntervalMin
|
||||
writeJSON(w, http.StatusOK, acc)
|
||||
}
|
||||
|
||||
// ── System stats handler ─────────────────────────────────────────────────
|
||||
|
||||
type diskStat struct {
|
||||
|
||||
Reference in New Issue
Block a user