#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* Updates the bundled disposable domains list from external sources.
*
* Usage:
* ./bin/update-domains [--dry-run] [--source=name] [--output=path]
*
* Options:
* --dry-run Show what would be done without writing
* --source=name Only fetch from specific source (can be repeated)
* --output=path Custom output path (default: src/Resources/disposable_domains.txt)
* --config=path Custom config file path
* --no-config Ignore config file
* --verbose Show progress details
*
* Configuration:
* Create a file named "disposable-blocker.php" in your project root:
*
* ```php
* return [
* 'sources' => [
* new UrlSource('https://example.com/domains.txt', 'my-source'),
* ],
* 'exclude_sources' => ['fakefilter'],
* 'output_path' => 'storage/disposable_domains.txt',
* ];
* ```
*/
// Find autoloader
$autoloadPaths = [
__DIR__ . '/../vendor/autoload.php', // Running from package directly
__DIR__ . '/../../../autoload.php', // Running from vendor/bin
getcwd() . '/vendor/autoload.php', // Running from project root
];
$autoloaded = false;
foreach ($autoloadPaths as $autoloadPath) {
if (is_file($autoloadPath)) {
require_once $autoloadPath;
$autoloaded = true;
break;
}
}
if (!$autoloaded) {
fwrite(STDERR, "Could not find autoload.php. Run composer install first.\n");
exit(1);
}
use Raul3k\DisposableBlocker\Core\Config\Configuration;
use Raul3k\DisposableBlocker\Core\Sources\SourceRegistry;
// Parse arguments
$options = getopt('', ['dry-run', 'source:', 'output:', 'config:', 'no-config', 'verbose', 'help']);
$args = array_slice($argv, 1);
if (isset($options['help']) || in_array('--help', $args) || in_array('-h', $args)) {
echo <<<HELP
Update Disposable Domains List
Usage:
./bin/update-domains [options]
vendor/bin/update-domains [options]
Options:
--dry-run Show statistics without writing the file
--source=name Only fetch from specific source (can be repeated)
--output=path Custom output path
--config=path Custom config file path
--no-config Ignore configuration file
--verbose Show detailed progress
--help Show this help message
Configuration File:
Create "disposable-blocker.php" in your project root:
<?php
use Raul3k\DisposableBlocker\Core\Sources\UrlSource;
use Raul3k\DisposableBlocker\Core\Sources\FileSource;
return [
// Add custom sources
'sources' => [
new UrlSource(
url: 'https://example.com/domains.txt',
name: 'my-custom-source'
),
new FileSource(
path: __DIR__ . '/my-domains.txt',
name: 'local-domains'
),
],
// Exclude built-in sources (optional)
'exclude_sources' => ['fakefilter'],
// Custom output path (optional)
'output_path' => __DIR__ . '/storage/disposable_domains.txt',
];
Built-in Sources:
- disposable-email-domains
- burner-email-providers
- mailchecker
- ivolo-disposable
- fakefilter
Examples:
./bin/update-domains
./bin/update-domains --dry-run
./bin/update-domains --source=disposable-email-domains --source=burner-email-providers
./bin/update-domains --output=storage/domains.txt
./bin/update-domains --config=custom-config.php
HELP;
exit(0);
}
$dryRun = isset($options['dry-run']) || in_array('--dry-run', $args);
$verbose = isset($options['verbose']) || in_array('--verbose', $args);
$noConfig = isset($options['no-config']) || in_array('--no-config', $args);
$configPath = $options['config'] ?? null;
// Output helpers
function info(string $message): void {
echo "\033[34m[INFO]\033[0m $message\n";
}
function success(string $message): void {
echo "\033[32m[OK]\033[0m $message\n";
}
function warning(string $message): void {
echo "\033[33m[WARN]\033[0m $message\n";
}
function error(string $message): void {
echo "\033[31m[ERROR]\033[0m $message\n";
}
function progress(string $message): void {
global $verbose;
if ($verbose) {
echo " ? $message\n";
}
}
// Main
echo "\n";
echo "??????????????????????????????????????????????????????????????\n";
echo "? Disposable Domains List Updater ?\n";
echo "??????????????????????????????????????????????????????????????\n";
echo "\n";
// Load configuration
$config = null;
if (!$noConfig) {
try {
if ($configPath !== null) {
$config = Configuration::fromFile($configPath);
info("Using config: $configPath");
} else {
$config = new Configuration();
if ($config->isLoaded()) {
info("Using config: " . $config->getConfigPath());
}
}
} catch (Throwable $e) {
error("Failed to load config: " . $e->getMessage());
exit(1);
}
}
// Setup registry with configuration
$registry = new SourceRegistry();
if ($config !== null && $config->isLoaded()) {
$config->applyTo($registry);
$customSources = $config->getSources();
if (!empty($customSources)) {
info("Custom sources: " . count($customSources));
}
$excluded = $config->getExcludeSources();
if (!empty($excluded)) {
info("Excluded sources: " . implode(', ', $excluded));
}
}
$availableSources = $registry->list();
// Determine output path
$defaultOutputPath = __DIR__ . '/../src/Resources/disposable_domains.txt';
if (isset($options['output'])) {
$outputPath = $options['output'];
} elseif ($config !== null && $config->getOutputPath() !== null) {
$outputPath = $config->getOutputPath();
} else {
$outputPath = $defaultOutputPath;
}
// Handle multiple --source options
$selectedSources = [];
if (isset($options['source'])) {
$selectedSources = is_array($options['source']) ? $options['source'] : [$options['source']];
}
// Validate selected sources
if (!empty($selectedSources)) {
foreach ($selectedSources as $source) {
if (!in_array($source, $availableSources)) {
error("Unknown source: $source");
echo "Available sources: " . implode(', ', $availableSources) . "\n";
exit(1);
}
}
$sourcesToFetch = $selectedSources;
} else {
$sourcesToFetch = $availableSources;
}
echo "\n";
info("Sources to fetch: " . implode(', ', $sourcesToFetch));
info("Output path: $outputPath");
echo "\n";
$allDomains = [];
$stats = [];
foreach ($sourcesToFetch as $sourceName) {
$source = $registry->get($sourceName);
echo "Fetching: \033[1m$sourceName\033[0m\n";
progress("URL: " . ($source->getUrl() ?? 'local file'));
$startTime = microtime(true);
$count = 0;
try {
foreach ($source->fetch() as $domain) {
$domain = strtolower(trim($domain));
if ($domain !== '' && !str_starts_with($domain, '#')) {
$allDomains[$domain] = true;
$count++;
}
}
$elapsed = round(microtime(true) - $startTime, 2);
$stats[$sourceName] = ['count' => $count, 'time' => $elapsed, 'status' => 'ok'];
success("Fetched $count domains in {$elapsed}s");
} catch (Throwable $e) {
$stats[$sourceName] = ['count' => 0, 'time' => 0, 'status' => 'error', 'error' => $e->getMessage()];
error("Failed: " . $e->getMessage());
}
echo "\n";
}
// Sort domains alphabetically
$domains = array_keys($allDomains);
sort($domains, SORT_STRING);
$totalDomains = count($domains);
// Statistics
echo "??????????????????????????????????????????????????????????????\n";
echo "? Statistics ?\n";
echo "??????????????????????????????????????????????????????????????\n";
echo "\n";
if (!empty($stats)) {
$maxNameLen = max(array_map('strlen', array_keys($stats)));
foreach ($stats as $name => $stat) {
$namePadded = str_pad($name, $maxNameLen);
if ($stat['status'] === 'ok') {
$countFormatted = number_format($stat['count']);
echo " $namePadded : $countFormatted domains ({$stat['time']}s)\n";
} else {
echo " $namePadded : \033[31mFAILED\033[0m - {$stat['error']}\n";
}
}
echo "\n";
echo " " . str_repeat('?', $maxNameLen + 30) . "\n";
}
echo " \033[1mTotal unique domains: " . number_format($totalDomains) . "\033[0m\n";
echo "\n";
if ($dryRun) {
warning("Dry run - no file written");
echo "\n";
exit(0);
}
// Ensure output directory exists
$outputDir = dirname($outputPath);
if (!is_dir($outputDir)) {
if (!mkdir($outputDir, 0755, true)) {
error("Failed to create output directory: $outputDir");
exit(1);
}
}
// Write file
info("Writing to: $outputPath");
$header = <<<HEADER
# Disposable Email Domains
# Auto-generated by bin/update-domains
# Last updated: %s
# Total domains: %s
#
# Sources:
%s
#
# This file is part of disposable-email-blocker-core
# https://github.com/raul3k/disposable-email-blocker-core
HEADER;
$sourcesComment = implode("\n", array_map(
fn($name) => "# - $name",
$sourcesToFetch
));
$content = sprintf(
$header,
date('Y-m-d H:i:s T'),
number_format($totalDomains),
$sourcesComment
);
$content .= implode("\n", $domains) . "\n";
$bytesWritten = file_put_contents($outputPath, $content);
if ($bytesWritten === false) {
error("Failed to write file!");
exit(1);
}
$sizeKb = round($bytesWritten / 1024, 1);
success("Written $sizeKb KB to $outputPath");
echo "\n";
|