Kevin Eaton

laravel-hash-list

Keep the list of people you must not email, without keeping their email addresses.

v1.0.0Read the docsGitHub · coming soonPackagist · coming soon

Laravel Hash List

Keep the list of people you must not email, without keeping their email addresses.

composer require keaton-php/laravel-hash-list
php artisan vendor:publish --tag="hash_list-migrations"
php artisan migrate

The service provider is discovered automatically, and the migration creates one table, hash_list_entries. At the end of this README, two organisations share their do-not-contact lists as hashes (one of them 100,000 long), check everyone against both before reaching out, and neither receives the other's addresses in plain text, or stores one.

The plain version

Say people can ask you to stop emailing them. The obvious way to remember them is a list, and in_array() to check it before you send:

<?php

$unsubscribed = ['jane@example.com', 'sam@example.com'];

foreach (['jane@example.com', 'riley@example.com'] as $email) {
    echo $email, ': ', in_array($email, $unsubscribed, true) ? 'skip' : 'send', PHP_EOL;
}
jane@example.com: skip
riley@example.com: send

For a handful of addresses, that is fine.

Where it breaks down

The trouble is that one email address has many spellings. Here is one address, the way four different parts of an app might hand it to you:

<?php

$unsubscribed = ['jane@example.com'];

$spellings = [
    'jane@example.com',
    'Jane@Example.com',
    ' jane@example.com',
    'Jane Doe <jane@example.com>',
];

foreach ($spellings as $email) {
    printf("%-30s %s\n", "'{$email}'", in_array($email, $unsubscribed, true) ? 'skip' : 'send');
}
'jane@example.com'             skip
'Jane@Example.com'             send
' jane@example.com'            send
'Jane Doe <jane@example.com>'  send

Three of the four go out to someone who asked you to stop. And there are two more problems:

  • Scale. The list has to be in memory to be checked. At a hundred thousand addresses, that is a lot to load for every send.
  • Privacy. The list is a pile of real people's addresses: the people who least want to hear from you. And when another organisation shares its list with you, it is often not addresses at all but hashes of them, which in_array() can't compare with an address.

The same thing with Hash List

HashList::add() puts an address on the list, and HashList::has() asks whether an address is on it:

<?php

use Keaton\HashList\Facades\HashList;

HashList::add('jane@example.com');

$spellings = [
    'jane@example.com',
    'Jane@Example.com',
    ' jane@example.com',
    'Jane Doe <jane@example.com>',
];

foreach ($spellings as $email) {
    printf("%-30s %s\n", "'{$email}'", HashList::has($email) ? 'skip' : 'send');
}
'jane@example.com'             skip
'Jane@Example.com'             skip
' jane@example.com'            skip
'Jane Doe <jane@example.com>'  skip

All four spellings are caught, the check is one indexed database query, and what is stored is a hash of the address, not the address.

Continue in the docs →