Files
archivmail/internal/api/smtpout_handlers.go
T
sysops c1a9004720 feat(PROJ-28): SMTP-Out Relay — DB-Konfiguration + Admin-Tab
- smtpoutconfig.Store: AES-256-GCM verschlüsseltes Passwort in DB (id=1 Singleton)
- Mailer: Reload() für Runtime-Konfigurationswechsel (sync.RWMutex)
- API: GET/PUT/DELETE /api/admin/smtp-out + POST /api/admin/smtp-out/test
- Admin-Tab: Host, Port, User, Passwort, TLS-Switch, From, Test-Button, Status-Badge
- Startup: Lädt DB-Konfiguration und aktiviert Mailer ohne Restart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 22:36:57 +02:00

119 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package api
import (
"encoding/json"
"net/http"
"github.com/archivmail/config"
"github.com/archivmail/internal/audit"
"github.com/archivmail/internal/smtpoutconfig"
)
// handleGetSMTPOut returns the current SMTP-Out relay config (password masked).
// GET /api/admin/smtp-out — superadmin only.
func (s *Server) handleGetSMTPOut(w http.ResponseWriter, r *http.Request) {
cfg, err := s.smtpOutStore.Get(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if cfg == nil {
writeJSON(w, http.StatusOK, map[string]interface{}{"configured": false})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{"configured": true, "config": cfg})
}
// handleSaveSMTPOut upserts the SMTP-Out relay config and reloads the mailer.
// PUT /api/admin/smtp-out — superadmin only.
func (s *Server) handleSaveSMTPOut(w http.ResponseWriter, r *http.Request) {
var body smtpoutconfig.SMTPOutConfig
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid body")
return
}
if body.Port == 0 {
body.Port = 587
}
sess := sessionFromCtx(r.Context())
if err := s.smtpOutStore.Save(r.Context(), body, sess.Username); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
// Reload the mailer at runtime — no restart needed
if full, err := s.smtpOutStore.GetWithPassword(r.Context()); err == nil && full != nil {
s.mailer.Reload(config.SMTPOutConfig{
Host: full.Host,
Port: full.Port,
User: full.User,
Password: full.Password,
TLS: full.TLS,
From: full.From,
})
}
if s.audlog != nil {
s.audlog.Log(audit.Entry{
EventType: "smtp_out_config_saved",
Username: sess.Username,
IPAddress: s.remoteIP(r),
Success: true,
})
}
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
}
// handleDeleteSMTPOut removes the SMTP-Out config and disables the mailer.
// DELETE /api/admin/smtp-out — superadmin only.
func (s *Server) handleDeleteSMTPOut(w http.ResponseWriter, r *http.Request) {
if err := s.smtpOutStore.Delete(r.Context()); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
// Disable mailer
s.mailer.Reload(config.SMTPOutConfig{})
sess := sessionFromCtx(r.Context())
if s.audlog != nil {
s.audlog.Log(audit.Entry{
EventType: "smtp_out_config_deleted",
Username: sess.Username,
IPAddress: s.remoteIP(r),
Success: true,
})
}
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
}
// handleTestSMTPOut sends a test email to the logged-in admin.
// POST /api/admin/smtp-out/test — superadmin only.
func (s *Server) handleTestSMTPOut(w http.ResponseWriter, r *http.Request) {
if !s.mailer.IsConfigured() {
writeError(w, http.StatusServiceUnavailable, "SMTP-Out nicht konfiguriert")
return
}
sess := sessionFromCtx(r.Context())
u, err := s.users.GetByUsername(sess.Username)
if err != nil || u.Email == "" {
writeError(w, http.StatusBadRequest, "Keine E-Mail-Adresse für diesen Account")
return
}
html := `<p>Dies ist eine Test-E-Mail von <strong>archivmail</strong>.<br>Der SMTP-Relay ist korrekt konfiguriert.</p>`
txt := "Dies ist eine Test-E-Mail von archivmail. Der SMTP-Relay ist korrekt konfiguriert."
if err := s.mailer.Send(u.Email, "archivmail SMTP-Test", html, txt); err != nil {
writeError(w, http.StatusBadGateway, "Versand fehlgeschlagen: "+err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": true,
"sent_to": u.Email,
})
}