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.
## 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)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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.
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.X-QQQ-Email-Idstays on every transport.EmailEffects.send(request, effectKey):QUEUED_OR_DUPLICATE: the email is in qbit-email's outbox exactly once, because the outbox dedupes by key.REFUSEDfor a suppressed or invalid recipient; a refusal is never retried.findByIdempotencyKeyandfindByRfcMessageId.EffectTarget(EmailJobEffectTarget, about 25 lines) is sketched indocs/DURABLE-JOBS.md. It goes in when QQQ RC.7 ships with #1191, because develop can't take a SNAPSHOT dependency.docs/ddl/email-message-id-upgrade.sql.Testing
./mvnw verifypasses 271 tests, with no failures and 94.67% instruction / 100% class coverage. Checkstyle on main and test is clean.Follow-ups
EmailJobEffectTargetwhen RC.7 is released.rfc_message_idchangeset in its email adoption; the upgrade SQL above is the reference.🤖 Generated with Claude Code