Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
composer.phar
/vendor/
composer.lock
.phpunit.result.cache
27 changes: 27 additions & 0 deletions scripts/refresh_disposable_list.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/bash

SRC=(
"https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/refs/heads/main/disposable_email_blocklist.conf"
"https://raw.githubusercontent.com/FGRibreau/mailchecker/refs/heads/master/list.txt"
)

OUTPUT_FILE="src/disposable_email_blocklist.conf"
if [ ! -f src/disposable_email_blocklist.conf ]; then
echo "run at repository top level";
exit 1;
fi

echo "" > $OUTPUT_FILE
for src in ${SRC[@]}; do
curl -s $src -o $OUTPUT_FILE.tmp
echo "--- $src ---";
wc -l $OUTPUT_FILE.tmp;
cat $OUTPUT_FILE.tmp >> $OUTPUT_FILE
rm $OUTPUT_FILE.tmp
done

cat src/disposable_email_blocklist.conf | sort | uniq > src/disposable_email_blocklist.conf.tmp
mv src/disposable_email_blocklist.conf.tmp src/disposable_email_blocklist.conf

echo "--- Total in $OUTPUT_FILE ---";
wc -l src/disposable_email_blocklist.conf
1 change: 1 addition & 0 deletions src/InvalidEmailException.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
class InvalidEmailException extends \Exception {
const SYNTAX = 10;
const DNSRECORDS = 20;
const DISPOSABLE = 30;
}
89 changes: 87 additions & 2 deletions src/MailUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ public static function normalize(string $address): string
}

/**
* Check if the address is valid, raise an exception if not.
*
* @param string $original_address_string
* @param bool $doDnsChecks
* @param bool $doDnsChecks If true, the address will be checked if it has DNS records to receive emails.
* @param bool $checkDisposable If true, the address will be checked if it is disposable.
* @param bool $useIndexCache If true, the index cache will be used (built on first use) to check if the address is disposable.
* @return Address
* @throws InvalidEmailException
*/
public static function address(string $original_address_string, bool $doDnsChecks = true): Address
public static function address(string $original_address_string, bool $doDnsChecks = true, bool $checkDisposable = true, bool $useIndexCache = true): Address
{
$address_string = self::normalize($original_address_string);
if (filter_var($address_string, FILTER_VALIDATE_EMAIL) !== $address_string) {
Expand Down Expand Up @@ -43,6 +47,11 @@ public static function address(string $original_address_string, bool $doDnsCheck
throw new InvalidEmailException("no dns record for domain", InvalidEmailException::DNSRECORDS);
}
}
if ($checkDisposable) {
if (self::isDisposable($address, $useIndexCache)) {
throw new InvalidEmailException("disposable address", InvalidEmailException::DISPOSABLE);
}
}
return $address;
}

Expand All @@ -55,4 +64,80 @@ public static function hasA(Address $address): bool
{
return checkdnsrr($address->getDomain(), "A");
}

/**
* Build the index for the disposable email block list
* @param string $filename
* @param bool $cache
* @return array<string,array<string,string>|null>
* @throws \Exception
*/
private static function buildIndex(string $filename, bool $cache = true): ?array
{
$cacheFilename = sys_get_temp_dir() . "/mailcheck_index_" . md5($filename) . ".bin";
if (file_exists($cacheFilename) && $cache) {
return unserialize(file_get_contents($cacheFilename));
}
$tree = [];
$f = fopen($filename, "r");
if ($f === false) {
return null;
}
while (($line = fgets($f)) !== false) {
$treeItem = &$tree;
foreach (str_split($line) as $char) {
if ($char === "\n") {
break;
}
if ($char === "0") {
$char = "00";
}
/** @phpstan-ignore-next-line non falsy value */
if (!array_key_exists($char, $treeItem)) {
$treeItem["$char"] = [];
$treeItem = &$treeItem["$char"];
} else {
$treeItem = &$treeItem["$char"];
}
}
}
if ($cache) {
file_put_contents($cacheFilename, serialize($tree));
}
return $tree;
}

private static function searchIndex(string $indexFilename, string $domain, bool $cache = true): ?bool
{
$wt = self::buildIndex($indexFilename, $cache);
if ($wt === null) {
return null;
}
foreach (str_split($domain) as $char) {
if ($char === "0") {
$char = "00";
}
if (!isset($wt["$char"])) {
return false;
}
$wt = $wt[$char];
}
return empty(array_keys($wt));
}

/**
* Check if address is disposable
*
* Domains list comes from
* https://github.com/disposable-email-domains/disposable-email-domains
* https://github.com/FGRibreau/mailchecker/blob/master/list.txt
*
* @param Address $address
* @param bool $cache
* @return bool|null
*/
public static function isDisposable(Address $address, bool $cache = true): ?bool
{
return self::searchIndex(__DIR__ . "/disposable_email_blocklist.conf", $address->getDomain(), $cache);
}
}
Loading