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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user