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>
This commit is contained in:
sysops
2026-03-31 22:36:57 +02:00
parent 7371a73b3e
commit c1a9004720
7 changed files with 698 additions and 8 deletions
+23 -8
View File
@@ -9,6 +9,7 @@ import (
"net"
"net/smtp"
"strings"
"sync"
"time"
"github.com/archivmail/config"
@@ -16,6 +17,7 @@ import (
// Mailer sends transactional emails via the configured SMTP-Out relay.
type Mailer struct {
mu sync.RWMutex
cfg config.SMTPOutConfig
}
@@ -24,30 +26,43 @@ func New(cfg config.SMTPOutConfig) *Mailer {
return &Mailer{cfg: cfg}
}
// Reload replaces the runtime configuration without restarting the process.
func (m *Mailer) Reload(cfg config.SMTPOutConfig) {
m.mu.Lock()
m.cfg = cfg
m.mu.Unlock()
}
// IsConfigured returns true when the smtp_out config is usable (host + from set).
func (m *Mailer) IsConfigured() bool {
m.mu.RLock()
defer m.mu.RUnlock()
return m.cfg.Host != "" && m.cfg.From != ""
}
// Send sends an HTML + plaintext email to a single recipient.
func (m *Mailer) Send(to, subject, htmlBody, textBody string) error {
if !m.IsConfigured() {
m.mu.RLock()
cfg := m.cfg
m.mu.RUnlock()
if cfg.Host == "" || cfg.From == "" {
return fmt.Errorf("mailer: smtp_out not configured")
}
addr := fmt.Sprintf("%s:%d", m.cfg.Host, port(m.cfg.Port))
addr := fmt.Sprintf("%s:%d", cfg.Host, port(cfg.Port))
msg := buildMIME(m.cfg.From, to, subject, htmlBody, textBody)
msg := buildMIME(cfg.From, to, subject, htmlBody, textBody)
var auth smtp.Auth
if m.cfg.User != "" {
auth = smtp.PlainAuth("", m.cfg.User, m.cfg.Password, m.cfg.Host)
if cfg.User != "" {
auth = smtp.PlainAuth("", cfg.User, cfg.Password, cfg.Host)
}
if m.cfg.TLS {
return sendTLS(addr, m.cfg.Host, auth, m.cfg.From, to, msg)
if cfg.TLS {
return sendTLS(addr, cfg.Host, auth, cfg.From, to, msg)
}
return sendSTARTTLS(addr, auth, m.cfg.From, to, msg)
return sendSTARTTLS(addr, auth, cfg.From, to, msg)
}
func port(p int) int {