A campaign emails an address at most once, ever

FeatureEmailService
Shipped
September 12, 2026 at 12:04 AM UTC
Author
Kamo
Commit
0fcb14a

Addresses are matched trimmed and case-insensitively, and a row the campaign has handed to the relay (sent, in flight, bounced, failed, or retried) is now its permanent record of that address: never deleted, reset or re-queued. - materializeRecipients used to delete every row and rebuild the list on each save, so editing a paused campaign put already-emailed addresses back in the queue. It now keeps history, refreshes rows not yet contacted in place, removes only uncontacted rows that left the list, and never re-adds an address the campaign has contacted, however many times it is added. - The dispatcher claims a row SENDING before the relay sees it. A crash between the relay accepting a message and the row being marked SENT used to leave it QUEUED and send it again; now it stays SENDING and a sweep closes it out as FAILED without retrying. - A recipient added while a campaign was paused never got a release time, so it was never sent and the campaign never finished. Release times now go to any queued row that lacks one. - A startup unique index on (campaign_id, lower(trim(email))) backs the rule at the table. The recipient list endpoint now returns each row's name, merge values, source and whether it was contacted, so reopening and re-saving a draft no longer erases personalization. The From address falls back to the bulk provider's, and the From name is actually sent (it was stored and dropped).

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing