feat(PROJ-5): AES-256-GCM Verschlüsselung, PostgreSQL Metadaten, Async Index Worker

- Storage: AES-256-GCM Verschlüsselung (keyfile, graceful fallback bei fehlendem Key)
- Storage: PostgreSQL emails-Tabelle mit Auto-Migration
- Storage: Save/Delete/Stats/FirstAndLastMail nutzen DB wenn verfügbar
- Index: Async IndexWorker (Go-Channel, Queue 1000, non-blocking Submit)
- SMTP: IndexCallback für async Indexierung nach Mail-Eingang
- main: Backfill beim Start (40 Mails migriert + indexiert)
- Bestehende Mails werden transparent entschlüsselt (Fallback auf Raw)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-03-14 20:26:50 +01:00
parent 850290b5ef
commit 7e68c7ab02
8 changed files with 750 additions and 36 deletions
+23 -7
View File
@@ -29,15 +29,20 @@ type Stats struct {
LastMailAt atomic.Value // time.Time of last accepted mail
}
// IndexCallback is called after a mail is successfully stored, with the raw
// bytes and the storage ID. Used to submit to the async index worker.
type IndexCallback func(raw []byte, id string)
// Daemon is the embedded receive-only SMTP server.
type Daemon struct {
cfg config.SMTPConfig
store *storage.Store
logger *slog.Logger
stats Stats
server *smtp.Server
mu sync.Mutex
running bool
cfg config.SMTPConfig
store *storage.Store
logger *slog.Logger
stats Stats
server *smtp.Server
mu sync.Mutex
running bool
indexCallback IndexCallback
}
// New creates a new SMTP Daemon. Call Start() to begin accepting connections.
@@ -51,6 +56,11 @@ func New(cfg config.SMTPConfig, store *storage.Store, logger *slog.Logger) *Daem
return d
}
// SetIndexCallback sets the function called after each successfully stored mail.
func (d *Daemon) SetIndexCallback(cb IndexCallback) {
d.indexCallback = cb
}
// Start launches the SMTP daemon in a background goroutine.
// It returns immediately; use Stop() for graceful shutdown.
func (d *Daemon) Start() error {
@@ -237,6 +247,12 @@ func (s *session) Data(r io.Reader) error {
s.daemon.stats.LastMailAt.Store(time.Now())
s.daemon.logger.Info("SMTP: mail stored", "id", id, "from", s.from,
"rcpts", strings.Join(s.rcpts, ","), "bytes", len(raw), "ip", s.remoteIP)
// Submit to async index worker if callback is set
if s.daemon.indexCallback != nil {
s.daemon.indexCallback(raw, id)
}
return nil
}