// Package hotfolder implementiert IMP-05: Anbindung eines Hot-Folder/ // Scanner-Eingangs für E-Mail-Anhänge/Dokumente außerhalb des // IMAP-Postfachs, analog zum Ingestion-Pfad. Kein Vorbild in archivmail // für diesen Zuschnitt — Neubau. package hotfolder import ( "context" _ "embed" "fmt" "github.com/jackc/pgx/v5/pgxpool" ) //go:embed migrations/0001_mail_hotfolder_processed.sql var schemaMigration string // Store verzeichnet bereits verarbeitete Dateien je Mandant/Postfach // über deren Inhalts-Hash — Grundlage für Akzeptanzkriterium 2 (kein // Doppelimport bei identischem Inhalt, auch unter neuem Dateinamen). type Store struct { pool *pgxpool.Pool } func NewStore(pool *pgxpool.Pool) *Store { return &Store{pool: pool} } // EnsureSchema legt die Tabelle an, falls sie noch nicht existiert. func (s *Store) EnsureSchema(ctx context.Context) error { if _, err := s.pool.Exec(ctx, schemaMigration); err != nil { return fmt.Errorf("hotfolder: schema anlegen: %w", err) } return nil } // IsProcessed prüft, ob contentHash für tenantSlug/mailboxName bereits // erfolgreich importiert wurde. func (s *Store) IsProcessed(ctx context.Context, tenantSlug, mailboxName, contentHash string) (bool, error) { var exists bool err := s.pool.QueryRow(ctx, ` SELECT EXISTS(SELECT 1 FROM mail_hotfolder_processed WHERE tenant_slug = $1 AND mailbox_name = $2 AND content_hash = $3) `, tenantSlug, mailboxName, contentHash).Scan(&exists) if err != nil { return false, fmt.Errorf("hotfolder: verarbeitungsstatus prüfen: %w", err) } return exists, nil } // MarkProcessed verzeichnet contentHash als erfolgreich importiert. func (s *Store) MarkProcessed(ctx context.Context, tenantSlug, mailboxName, contentHash, filename string) error { if _, err := s.pool.Exec(ctx, ` INSERT INTO mail_hotfolder_processed (tenant_slug, mailbox_name, content_hash, filename) VALUES ($1, $2, $3, $4) ON CONFLICT (tenant_slug, mailbox_name, content_hash) DO NOTHING `, tenantSlug, mailboxName, contentHash, filename); err != nil { return fmt.Errorf("hotfolder: als verarbeitet markieren: %w", err) } return nil }