feat: fixed Message-ID per email and an effect handoff API for durable jobs #12

Merged
KofTwentyTwo merged 2 commits from feature/effect-message-id into develop 2026-10-14 14:30:02 +00:00
KofTwentyTwo commented 2026-10-14 13:59:06 +00:00 (Migrated from github.com)

Summary

This is the qbit-email side of the durable jobs side-effects outbox (QRun-IO/qqq#1191, spec section 2.13): an email effect is sent with a fixed Message-ID, and a job can hand an email to qbit-email exactly once per key.

  • Deterministic Message-ID.
    • Each queued message stores one RFC 5322 Message-ID in a new unique column, email_message.rfc_message_id. It is derived from the idempotency key (or a stable per-message seed when there is none), and every attempt reuses it.
    • SMTP (including Gmail SMTP), Postmark, SendGrid and Mailgun send it.
    • SES receives it in the raw MIME but replaces it downstream; this is documented.
    • X-QQQ-Email-Id stays on every transport.
  • EmailEffects.send(request, effectKey):
    • Returns QUEUED_OR_DUPLICATE: the email is in qbit-email's outbox exactly once, because the outbox dedupes by key.
    • Returns REFUSED for a suppressed or invalid recipient; a refusal is never retried.
    • A request that already carries a different key is refused.
  • Reconcile lookups: findByIdempotencyKey and findByRfcMessageId.
  • Binding class: the class that implements core's EffectTarget (EmailJobEffectTarget, about 25 lines) is sketched in docs/DURABLE-JOBS.md. It goes in when QQQ RC.7 ships with #1191, because develop can't take a SNAPSHOT dependency.
  • Upgrade SQL for existing Postgres installs: docs/ddl/email-message-id-upgrade.sql.
  • Checkstyle now also covers test sources, per the QRun qbit repo standard, and the existing violations are fixed.

Testing

  • ./mvnw verify passes 271 tests, with no failures and 94.67% instruction / 100% class coverage. Checkstyle on main and test is clean.
  • New tests cover:
    • the Message-ID is stored once and identical across retries;
    • each HTTP provider's request carries it;
    • the same key gives the same Message-ID;
    • one row per key, with the second send returning a duplicate;
    • the conflicting-key refusal and the suppressed-recipient refusal;
    • both lookups.

Follow-ups

  • Add EmailJobEffectTarget when RC.7 is released.
  • kof22 needs an rfc_message_id changeset in its email adoption; the upgrade SQL above is the reference.

🤖 Generated with Claude Code

## Summary This is the qbit-email side of the durable jobs side-effects outbox (QRun-IO/qqq#1191, spec section 2.13): an email effect is sent with a **fixed Message-ID**, and a job can hand an email to qbit-email **exactly once per key**. - **Deterministic Message-ID.** - Each queued message stores one RFC 5322 Message-ID in a new unique column, `email_message.rfc_message_id`. It is derived from the idempotency key (or a stable per-message seed when there is none), and every attempt reuses it. - SMTP (including Gmail SMTP), Postmark, SendGrid and Mailgun send it. - SES receives it in the raw MIME but replaces it downstream; this is documented. - `X-QQQ-Email-Id` stays on every transport. - **`EmailEffects.send(request, effectKey)`:** - Returns `QUEUED_OR_DUPLICATE`: the email is in qbit-email's outbox exactly once, because the outbox dedupes by key. - Returns `REFUSED` for a suppressed or invalid recipient; a refusal is never retried. - A request that already carries a different key is refused. - **Reconcile lookups:** `findByIdempotencyKey` and `findByRfcMessageId`. - **Binding class:** the class that implements core's `EffectTarget` (`EmailJobEffectTarget`, about 25 lines) is sketched in `docs/DURABLE-JOBS.md`. It goes in when QQQ RC.7 ships with #1191, because develop can't take a SNAPSHOT dependency. - **Upgrade SQL** for existing Postgres installs: `docs/ddl/email-message-id-upgrade.sql`. - **Checkstyle** now also covers test sources, per the QRun qbit repo standard, and the existing violations are fixed. ## Testing - `./mvnw verify` passes 271 tests, with no failures and 94.67% instruction / 100% class coverage. Checkstyle on main and test is clean. - New tests cover: - the Message-ID is stored once and identical across retries; - each HTTP provider's request carries it; - the same key gives the same Message-ID; - one row per key, with the second send returning a duplicate; - the conflicting-key refusal and the suppressed-recipient refusal; - both lookups. ## Follow-ups - Add `EmailJobEffectTarget` when RC.7 is released. - kof22 needs an `rfc_message_id` changeset in its email adoption; the upgrade SQL above is the reference. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.