feat(PROJ-45): IMAP Per-Folder UID-Tracking + UIDVALIDITY-Check

- FolderState: GetFolderState, UpsertFolderState, ListFolderStates, DecideResync
- syncFolder nutzt per-folder UID-Tracking statt globalem highest_uid
- UIDVALIDITY-Check loest automatisch Full-Resync aus
- imap_folder_state Tabelle in initSchema (CREATE TABLE IF NOT EXISTS)
- SetAuditLogger in main.go verdrahtet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-05-11 10:49:14 +02:00
parent 16013e8b66
commit 4151b6f8c5
5 changed files with 338 additions and 16 deletions
+19
View File
@@ -86,6 +86,20 @@ ALTER TABLE imap_accounts ADD COLUMN IF NOT EXISTS sync_error_msg TEXT NOT NULL
ALTER TABLE imap_accounts ADD COLUMN IF NOT EXISTS tenant_id INTEGER REFERENCES tenants(id);
`
// folderStateSQL creates the per-folder UID-tracking table introduced by PROJ-45.
// Replaces the previous account-global last_uid tracking which silently skipped
// folders with lower UIDs than INBOX and ignored UIDVALIDITY changes.
const folderStateSQL = `
CREATE TABLE IF NOT EXISTS imap_folder_state (
account_id BIGINT NOT NULL REFERENCES imap_accounts(id) ON DELETE CASCADE,
folder TEXT NOT NULL,
last_uid BIGINT NOT NULL DEFAULT 0,
uid_validity BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (account_id, folder)
);
`
// New creates a new Store, connects to PostgreSQL, and runs the migration.
func New(dsn, secret string) (*Store, error) {
pool, err := pgxpool.New(context.Background(), dsn)
@@ -103,6 +117,11 @@ func New(dsn, secret string) (*Store, error) {
return nil, fmt.Errorf("imap store: migrate alter: %w", err)
}
if _, err := pool.Exec(context.Background(), folderStateSQL); err != nil {
pool.Close()
return nil, fmt.Errorf("imap store: migrate folder state: %w", err)
}
key := sha256.Sum256([]byte(secret))
return &Store{pool: pool, encKey: key}, nil
}