Add Cloudflare email and collaboration workflows

This commit is contained in:
2026-07-22 12:59:16 -04:00
parent 09f51d1ef4
commit 4454e918c2
35 changed files with 1596 additions and 487 deletions

View File

@@ -3,6 +3,7 @@ package email
import (
"context"
"database/sql"
"errors"
"fmt"
"log/slog"
"math"
@@ -211,17 +212,28 @@ func (q *Queue) deliver(ctx context.Context, row QueueRow) error {
}
// Failure: retry or give up.
if row.Attempts >= row.MaxAttempts {
var permanent *PermanentError
if errors.As(err, &permanent) {
if _, ferr := q.db.ExecContext(ctx, `
UPDATE email_queue SET status = 'failed', last_error = ? WHERE id = ?
`, truncateErr(err.Error()), row.ID); ferr != nil {
return fmt.Errorf("mark permanent failure: %w", ferr)
}
q.logger.Warn("email permanently rejected", "id", row.ID, "attempts", row.Attempts+1, "error", err)
return nil
}
currentAttempt := row.Attempts + 1
if currentAttempt >= row.MaxAttempts {
if _, ferr := q.db.ExecContext(ctx, `
UPDATE email_queue SET status = 'failed', last_error = ? WHERE id = ?
`, truncateErr(err.Error()), row.ID); ferr != nil {
return fmt.Errorf("mark failed: %w", ferr)
}
q.logger.Warn("email permanently failed", "id", row.ID, "attempts", row.Attempts, "error", err)
q.logger.Warn("email permanently failed", "id", row.ID, "attempts", currentAttempt, "error", err)
return nil
}
backoff := q.backoff(row.Attempts)
backoff := q.backoff(currentAttempt)
next := time.Now().UTC().Add(backoff)
if _, err := q.db.ExecContext(ctx, `
UPDATE email_queue SET status = 'pending', next_attempt_at = ?, last_error = ? WHERE id = ?
@@ -229,9 +241,9 @@ func (q *Queue) deliver(ctx context.Context, row QueueRow) error {
return fmt.Errorf("schedule retry: %w", err)
}
// TemporaryError (Postmark 5xx) is retried with the same backoff as
// other errors; the max-attempts cap still bounds total work.
q.logger.Info("email retry scheduled", "id", row.ID, "attempt", row.Attempts, "backoff", backoff, "error", err)
// Temporary provider failures and unclassified sender failures are retried;
// PermanentError is handled above without wasting additional attempts.
q.logger.Info("email retry scheduled", "id", row.ID, "attempt", currentAttempt, "backoff", backoff, "error", err)
return nil
}