Skip to content

feat: delivery preflight - detect bounces on prior sends before send-email - #1033

Merged
aaronjmars merged 1 commit into
mainfrom
feat/email-bounce-preflight
Sep 6, 2026
Merged

feat: delivery preflight - detect bounces on prior sends before send-email#1033
aaronjmars merged 1 commit into
mainfrom
feat/email-bounce-preflight

Conversation

@aaronjmars

Copy link
Copy Markdown
Collaborator

Why

A 200 from POST /emails only means Resend accepted the message - delivered
vs bounced is decided asynchronously. So a send logged email-sent (http 200 +
resend_id) can still have bounced (dead mailbox / NoEmail, spam complaint)
with the instance none the wiser.

What

At the start of send-email, run a delivery preflight over earlier sends:

  • scripts/check_email_bounces.py - polls Resend GET /emails/{id} (reads
    RESEND_API_KEY from env, never argv) for every memory/email-log.json row
    without a terminal delivery_status, 30-day lookback.
  • Writes the outcome back: delivery_status (delivered / bounced / complained /
    failed / pending), last_event, bounce (type/subType/message when present),
    delivery_checked_at.
  • On a hard bounce, flips the source draft to status: contact-unverified.
  • Prints BOUNCE_ALERT + one line per new bounce; the skill then ./notifys the
    operator so a human handles the contact.

Advisory only - unset key, empty ledger, or a poll error just no-ops; never
blocks the send. Poll-based (fits aeon's cron/serverless model, no webhook endpoint).

Notes

  • Canon ships send-email; disclosure-emailer is instance-local (aeon-vuln /
    aeon-onchain) and gets the same preflight there.
  • No new egress: the poll hits api.resend.com, already send-email's allowed host.
  • eyebrowlock regenerated: send-email (this change) + two pre-existing stale
    hashes (skill-health, deploy-uni-hook) that the drift gate requires refreshed
    in the same PR (content-hash only, no reach change). eyebrow verify --ci clean.

…email

A 200 from POST /emails only means 'accepted'; the delivered/bounced outcome is
async, so a send logged email-sent can still have bounced (dead mailbox, spam
complaint) invisibly. Add scripts/check_email_bounces.py, run at the START of
send-email: it polls Resend GET /emails/{id} for every memory/email-log.json row
without a terminal delivery_status, writes the outcome back (delivery_status/
last_event/bounce/delivery_checked_at), flags a hard-bounced draft
status: contact-unverified, and prints BOUNCE_ALERT so the skill notifies the
operator. Advisory only (unset key / poll error no-ops). No new egress.

Regenerated eyebrowlock: send-email (this change) plus two pre-existing stale
hashes (skill-health, deploy-uni-hook) the drift gate requires refreshed.
@aaronjmars
aaronjmars merged commit 21b82db into main Sep 6, 2026
5 checks passed
@aaronjmars
aaronjmars deleted the feat/email-bounce-preflight branch September 6, 2026 16:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant