Issue summary
Ghost's boot-time newsletter recovery handles recent submitting emails, but it does not recover a recent pending email when the process exits after the email row is persisted and before its background send job is scheduled.
This is adjacent to, but distinct from, the interrupted-send recovery merged in #27822.
Minimal reproduction
- Use a sendable post/newsletter and create an email row with
status: pending and a positive recipient count.
- Simulate process termination after persistence succeeds but before
scheduleEmail() has added the background job (so no email batches or recipients have been materialized).
- Restart Ghost within
bulkEmail:resumeMaxAgeMs.
- Observe that the boot-time scanner only queries
status:submitting; the eligible pending row is never scheduled and remains pending.
Expected behavior
A boot-only recovery pass should conservatively re-enter the ordinary CAS-protected scheduling path for a row only when it is recent and clearly represents this pre-scheduling crash window. It must not broadly resend pending newsletters or interfere with already-materialized work.
A candidate implementation applies these safeguards:
- only rows created before the current service start and within the configured recovery-age bound;
- only sendable parent posts;
- no existing email batches or recipients;
- normal status locking remains the final deduplication authority;
- rows outside the narrow eligibility predicate remain untouched for operator review.
Why this matters
The email row is durable before the job is scheduled. A crash in that interval otherwise leaves an email that is neither queued for sending nor surfaced as a failed send.
Version and environment
Observed with Ghost v6.57.0 and reproduced against MySQL 8 using a controlled test harness. The affected logic remains present on main at the time of filing.
Proposed follow-up
I will open a focused draft PR with integration and unit coverage for this specific crash window. The test transport is mocked; no real recipient or provider data is involved.
Issue summary
Ghost's boot-time newsletter recovery handles recent
submittingemails, but it does not recover a recentpendingemail when the process exits after the email row is persisted and before its background send job is scheduled.This is adjacent to, but distinct from, the interrupted-send recovery merged in #27822.
Minimal reproduction
status: pendingand a positive recipient count.scheduleEmail()has added the background job (so no email batches or recipients have been materialized).bulkEmail:resumeMaxAgeMs.status:submitting; the eligiblependingrow is never scheduled and remains pending.Expected behavior
A boot-only recovery pass should conservatively re-enter the ordinary CAS-protected scheduling path for a row only when it is recent and clearly represents this pre-scheduling crash window. It must not broadly resend pending newsletters or interfere with already-materialized work.
A candidate implementation applies these safeguards:
Why this matters
The email row is durable before the job is scheduled. A crash in that interval otherwise leaves an email that is neither queued for sending nor surfaced as a failed send.
Version and environment
Observed with Ghost v6.57.0 and reproduced against MySQL 8 using a controlled test harness. The affected logic remains present on
mainat the time of filing.Proposed follow-up
I will open a focused draft PR with integration and unit coverage for this specific crash window. The test transport is mocked; no real recipient or provider data is involved.