How not to send the wrong email to the wrong person
In email campaigns, sending is not the hard part. The hard part is not sending the wrong email to the wrong person. That is why most of the code in my campaign system is defensive.
One decision point
Every recommendation passes through a single place. That place does not return the first reason for skipping, it returns all of them. So in a preview I can see exactly why someone drops off the list.
public function evaluate(User $user, string $channel, ?CarbonInterface $now = null): PolicyDecision
{
$now = ($now ?? Carbon::now())->copy();
$reasons = []; // collect ALL reasons, not just the first, so a preview can explain every skip
if (! (bool) $this->cfg('enabled', false)) {
$reasons[] = 'kill_switch_off';
}
if ($user->is_banned || $user->deleted_at) {
$reasons[] = 'account_blocked';
}
if ($channel === self::CHANNEL_EMAIL) {
if (! (bool) $user->is_subscribed) {
$reasons[] = 'no_marketing_consent';
}
if ($this->emailSuppressed(trim((string) $user->email))) {
$reasons[] = 'email_suppressed';
}
} elseif ($channel === self::CHANNEL_PUSH) {
if ($this->isQuietHour($now)) {
$reasons[] = 'quiet_hours';
}
}
// ... weekly cap from the shared marketing ledger, then the holdout control group
}
An excerpt of real code, anonymised.
The safeguards
- A switch for every campaign, off by default.
- A dry run with no sending.
- A recipient limit per run.
- A cool-down for the same recipient.
- A suppression list and consent check.
- A shared weekly cap, and quiet hours for push notifications.
A control group
I measure a campaign's effect with a control group: the control group gets a calculated recommendation but not the email. The difference shows what the sending actually brought.
Working on a similar problem?
If you are stuck, or want a second look at your solution, write to me. I am happy to look.
Write to me →More articles
Uploading large files in Laravel without breaking
How I solved uploading large videos so that one interrupted request does not take the whole upload with it.
Read →Rewriting a legacy system while it runs
How I replace an old system while users keep working: identical behaviour, a list of differences, audit logging.
Read →