Skip to content
Rapcsány Krisztián Rapcsány Krisztián
hu
← All articles
2 min read

How not to send the wrong email to the wrong person

Email Laravel Marketing

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.

RecommendationPolicy.php
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 →
Get a quote →