Files
archivmail/internal/api/import_handlers.go
T
sysops d79e334029 refactor: server.go in separate Handler-Dateien aufgeteilt
server.go (2357 -> 391 Zeilen) enthaelt nur noch Server-Struct,
Konstruktor, Router, Middleware und Hilfsfunktionen.

Neue Dateien:
- auth_handlers.go: Login, Logout, Me
- search_handlers.go: Suche, Mail-Anzeige, Anhaenge, Raw-Download
- admin_handlers.go: User-CRUD, SMTP/Storage-Stats, Services, Security
- import_handlers.go: IMAP + POP3 Account-Verwaltung und Import
- dashboard_handlers.go: System-Stats, Audit-Log

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 11:55:21 +01:00

548 lines
15 KiB
Go

package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/archivmail/internal/auth"
imapstore "github.com/archivmail/internal/imap"
pop3store "github.com/archivmail/internal/pop3"
"github.com/archivmail/internal/userstore"
)
// ── IMAP handlers ─────────────────────────────────────────────────────────
func (s *Server) handleListImap(w http.ResponseWriter, r *http.Request) {
if s.imapStore == nil {
writeError(w, http.StatusServiceUnavailable, "IMAP not configured")
return
}
sess := sessionFromCtx(r.Context())
// SEC-03: Use HasRole to correctly check admin privileges (domain_admin, admin, superadmin).
isAdmin := auth.HasRole(sess.Role, userstore.RoleDomainAdmin)
accounts, err := s.imapStore.List(r.Context(), sess.Username, isAdmin)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to list IMAP accounts")
return
}
if accounts == nil {
accounts = []imapstore.Account{}
}
writeJSON(w, http.StatusOK, accounts)
}
func (s *Server) handleCreateImap(w http.ResponseWriter, r *http.Request) {
if s.imapStore == nil {
writeError(w, http.StatusServiceUnavailable, "IMAP not configured")
return
}
var req struct {
Name string `json:"name"`
Host string `json:"host"`
Port int `json:"port"`
TLS string `json:"tls"`
Username string `json:"username"`
Password string `json:"password"`
ExcludedFolders []string `json:"excluded_folders"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.Name == "" || req.Host == "" || req.Username == "" || req.Password == "" {
writeError(w, http.StatusBadRequest, "name, host, username and password are required")
return
}
if req.Port <= 0 {
req.Port = 993
}
if req.TLS == "" {
req.TLS = "ssl"
}
if req.ExcludedFolders == nil {
req.ExcludedFolders = []string{}
}
sess := sessionFromCtx(r.Context())
acc := imapstore.Account{
Owner: sess.Username,
Name: req.Name,
Host: req.Host,
Port: req.Port,
TLS: req.TLS,
Username: req.Username,
ExcludedFolders: req.ExcludedFolders,
}
created, err := s.imapStore.Create(r.Context(), acc, req.Password)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to create IMAP account")
return
}
writeJSON(w, http.StatusCreated, created)
}
func (s *Server) handleDeleteImap(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 && !auth.HasRole(sess.Role, userstore.RoleDomainAdmin) {
writeError(w, http.StatusForbidden, "access denied")
return
}
if err := s.imapStore.Delete(r.Context(), id); err != nil {
writeError(w, http.StatusInternalServerError, "failed to delete account")
return
}
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
}
func (s *Server) handleTestImap(w http.ResponseWriter, r *http.Request) {
var req struct {
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
}
if req.Host == "" || req.Username == "" || req.Password == "" {
writeError(w, http.StatusBadRequest, "host, username and password are required")
return
}
if req.Port <= 0 {
req.Port = 993
}
if req.TLS == "" {
req.TLS = "ssl"
}
c, err := imapstore.Connect(req.Host, req.Port, req.TLS)
if err != nil {
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": false,
"error": fmt.Sprintf("Verbindung fehlgeschlagen: %v", err),
})
return
}
defer c.Close()
if err := c.Login(req.Username, req.Password).Wait(); err != nil {
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": false,
"error": fmt.Sprintf("Anmeldung fehlgeschlagen: %v", err),
})
return
}
folders, err := imapstore.ListFolders(c)
if err != nil {
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": false,
"error": fmt.Sprintf("Ordner konnten nicht gelesen werden: %v", err),
})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": true,
"folders": folders,
})
}
func (s *Server) handleStartImport(w http.ResponseWriter, r *http.Request) {
if s.imapStore == nil || s.imapImporter == 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 && !auth.HasRole(sess.Role, userstore.RoleDomainAdmin) {
writeError(w, http.StatusForbidden, "access denied")
return
}
if acc.Status == "running" {
writeError(w, http.StatusConflict, "import already running")
return
}
go s.imapImporter.Run(context.Background(), id)
// Return current account state (status will switch to "running" shortly)
acc.Status = "running"
writeJSON(w, http.StatusOK, acc)
}
func (s *Server) handleImapProgress(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 && !auth.HasRole(sess.Role, userstore.RoleDomainAdmin) {
writeError(w, http.StatusForbidden, "access denied")
return
}
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 && !auth.HasRole(sess.Role, userstore.RoleDomainAdmin) {
writeError(w, http.StatusForbidden, "access denied")
return
}
if err := s.imapScheduler.TriggerSync(r.Context(), id); err != nil {
s.logger.Error("trigger sync failed", "err", err)
writeError(w, http.StatusConflict, "sync already running or failed to start")
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 && !auth.HasRole(sess.Role, userstore.RoleDomainAdmin) {
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)
}
// ── POP3 handlers ──────────────────────────────────────────────────────────
func (s *Server) handleListPop3(w http.ResponseWriter, r *http.Request) {
if s.pop3Store == nil {
writeError(w, http.StatusServiceUnavailable, "POP3 not configured")
return
}
sess := sessionFromCtx(r.Context())
// SEC-03: Use HasRole to correctly check admin privileges (domain_admin, admin, superadmin).
isAdmin := auth.HasRole(sess.Role, userstore.RoleDomainAdmin)
accounts, err := s.pop3Store.List(r.Context(), sess.Username, isAdmin)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to list POP3 accounts")
return
}
if accounts == nil {
accounts = []pop3store.Account{}
}
writeJSON(w, http.StatusOK, accounts)
}
func (s *Server) handleCreatePop3(w http.ResponseWriter, r *http.Request) {
if s.pop3Store == nil {
writeError(w, http.StatusServiceUnavailable, "POP3 not configured")
return
}
var req struct {
Name string `json:"name"`
Host string `json:"host"`
Port int `json:"port"`
TLS string `json:"tls"`
TLSSkipVerify bool `json:"tls_skip_verify"`
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
}
if req.Name == "" || req.Host == "" || req.Username == "" || req.Password == "" {
writeError(w, http.StatusBadRequest, "name, host, username and password are required")
return
}
if req.Port <= 0 {
req.Port = 110
}
if req.TLS == "" {
req.TLS = "none"
}
sess := sessionFromCtx(r.Context())
acc := pop3store.Account{
Owner: sess.Username,
Name: req.Name,
Host: req.Host,
Port: req.Port,
TLS: req.TLS,
TLSSkipVerify: req.TLSSkipVerify,
Username: req.Username,
}
created, err := s.pop3Store.Create(r.Context(), acc, req.Password)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to create POP3 account")
return
}
writeJSON(w, http.StatusCreated, created)
}
func (s *Server) handleDeletePop3(w http.ResponseWriter, r *http.Request) {
if s.pop3Store == nil {
writeError(w, http.StatusServiceUnavailable, "POP3 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.pop3Store.Get(r.Context(), id)
if err != nil {
writeError(w, http.StatusNotFound, "account not found")
return
}
sess := sessionFromCtx(r.Context())
if acc.Owner != sess.Username && !auth.HasRole(sess.Role, userstore.RoleDomainAdmin) {
writeError(w, http.StatusForbidden, "access denied")
return
}
if err := s.pop3Store.Delete(r.Context(), id); err != nil {
writeError(w, http.StatusInternalServerError, "failed to delete account")
return
}
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
}
func (s *Server) handleTestPop3(w http.ResponseWriter, r *http.Request) {
var req struct {
Host string `json:"host"`
Port int `json:"port"`
TLS string `json:"tls"`
TLSSkipVerify bool `json:"tls_skip_verify"`
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
}
if req.Host == "" || req.Username == "" || req.Password == "" {
writeError(w, http.StatusBadRequest, "host, username and password are required")
return
}
if req.Port <= 0 {
req.Port = 110
}
if req.TLS == "" {
req.TLS = "none"
}
c, err := pop3store.Dial(req.Host, req.Port, req.TLS, req.TLSSkipVerify)
if err != nil {
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": false,
"message": fmt.Sprintf("Verbindung fehlgeschlagen: %v", err),
})
return
}
defer c.Close()
if err := c.Login(req.Username, req.Password); err != nil {
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": false,
"message": fmt.Sprintf("Anmeldung fehlgeschlagen: %v", err),
})
return
}
count, totalSize, err := c.Stat()
if err != nil {
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": false,
"message": fmt.Sprintf("STAT fehlgeschlagen: %v", err),
})
return
}
_ = c.Quit()
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": true,
"message": fmt.Sprintf("Verbindung erfolgreich: %d E-Mails", count),
"message_count": count,
"total_size_bytes": totalSize,
})
}
func (s *Server) handleStartPop3Import(w http.ResponseWriter, r *http.Request) {
if s.pop3Store == nil || s.pop3Importer == nil {
writeError(w, http.StatusServiceUnavailable, "POP3 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.pop3Store.Get(r.Context(), id)
if err != nil {
writeError(w, http.StatusNotFound, "account not found")
return
}
sess := sessionFromCtx(r.Context())
if acc.Owner != sess.Username && !auth.HasRole(sess.Role, userstore.RoleDomainAdmin) {
writeError(w, http.StatusForbidden, "access denied")
return
}
if acc.Status == "running" {
writeError(w, http.StatusConflict, "import already running")
return
}
go s.pop3Importer.Run(context.Background(), id)
// Return current account state (status will switch to "running" shortly)
acc.Status = "running"
writeJSON(w, http.StatusOK, acc)
}
func (s *Server) handlePop3Progress(w http.ResponseWriter, r *http.Request) {
if s.pop3Store == nil {
writeError(w, http.StatusServiceUnavailable, "POP3 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.pop3Store.Get(r.Context(), id)
if err != nil {
writeError(w, http.StatusNotFound, "account not found")
return
}
sess := sessionFromCtx(r.Context())
if acc.Owner != sess.Username && !auth.HasRole(sess.Role, userstore.RoleDomainAdmin) {
writeError(w, http.StatusForbidden, "access denied")
return
}
writeJSON(w, http.StatusOK, acc)
}