feat(PROJ-14): POP3-Import — Client, Store, Importer, API-Routen, Frontend-Seite
This commit is contained in:
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/archivmail/internal/auth"
|
||||
imapstore "github.com/archivmail/internal/imap"
|
||||
"github.com/archivmail/internal/index"
|
||||
pop3store "github.com/archivmail/internal/pop3"
|
||||
"github.com/archivmail/internal/smtpd"
|
||||
"github.com/archivmail/internal/storage"
|
||||
"github.com/archivmail/internal/userstore"
|
||||
@@ -46,6 +47,8 @@ type Server struct {
|
||||
imapStore *imapstore.Store
|
||||
imapImporter *imapstore.Importer
|
||||
imapScheduler *imapstore.Scheduler
|
||||
pop3Store *pop3store.Store
|
||||
pop3Importer *pop3store.Importer
|
||||
uploadJobs sync.Map // jobID → *UploadJob
|
||||
}
|
||||
|
||||
@@ -61,6 +64,12 @@ func (s *Server) SetImap(store *imapstore.Store, importer *imapstore.Importer, s
|
||||
s.imapScheduler = scheduler
|
||||
}
|
||||
|
||||
// SetPop3 wires the POP3 store and importer into the API server after construction.
|
||||
func (s *Server) SetPop3(store *pop3store.Store, importer *pop3store.Importer) {
|
||||
s.pop3Store = store
|
||||
s.pop3Importer = importer
|
||||
}
|
||||
|
||||
// New creates and wires up a new API server.
|
||||
func New(
|
||||
cfg config.APIConfig,
|
||||
@@ -129,6 +138,14 @@ func (s *Server) routes() {
|
||||
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))
|
||||
|
||||
// POP3 routes (accessible to all authenticated users)
|
||||
s.mux.HandleFunc("GET /api/pop3", s.authMiddleware(s.handleListPop3))
|
||||
s.mux.HandleFunc("POST /api/pop3", s.authMiddleware(s.handleCreatePop3))
|
||||
s.mux.HandleFunc("DELETE /api/pop3/{id}", s.authMiddleware(s.handleDeletePop3))
|
||||
s.mux.HandleFunc("POST /api/pop3/test", s.authMiddleware(s.handleTestPop3))
|
||||
s.mux.HandleFunc("POST /api/pop3/{id}/import", s.authMiddleware(s.handleStartPop3Import))
|
||||
s.mux.HandleFunc("GET /api/pop3/{id}/progress", s.authMiddleware(s.handlePop3Progress))
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler.
|
||||
@@ -1696,6 +1713,230 @@ func (s *Server) handleSecurityFix(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"message": msg})
|
||||
}
|
||||
|
||||
// ── 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())
|
||||
isAdmin := sess.Role == userstore.RoleAdmin
|
||||
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 && sess.Role != userstore.RoleAdmin {
|
||||
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 && sess.Role != userstore.RoleAdmin {
|
||||
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 && sess.Role != userstore.RoleAdmin {
|
||||
writeError(w, http.StatusForbidden, "access denied")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, acc)
|
||||
}
|
||||
|
||||
// sshConfigSet sets or replaces a directive in /etc/ssh/sshd_config.
|
||||
// Commented-out lines are left untouched; the active directive is updated or appended.
|
||||
func sshConfigSet(key, value string) error {
|
||||
|
||||
Reference in New Issue
Block a user