PHPackages                             nalabdou/disposable-email-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Mail &amp; Notifications](/categories/mail)
4. /
5. nalabdou/disposable-email-bundle

ActiveSymfony-bundle[Mail &amp; Notifications](/categories/mail)

nalabdou/disposable-email-bundle
================================

Production-grade Symfony bundle to detect and block disposable email addresses. Ships with 100K+ domains, extensible loaders, PSR-6 caching, Twig helpers, and a sync console command.

v1.0.0(2mo ago)01MITPHPPHP &gt;=8.2CI passing

Since Apr 6Pushed 2mo agoCompare

[ Source](https://github.com/nalabdou/disposable-email-bundle)[ Packagist](https://packagist.org/packages/nalabdou/disposable-email-bundle)[ RSS](/packages/nalabdou-disposable-email-bundle/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (14)Versions (3)Used By (0)

DisposableEmailBundle
=====================

[](#disposableemailbundle)

**The most complete Symfony bundle for detecting and blocking disposable (temporary) email addresses.**

Ships with a built-in domain list, extensible loader/whitelist architecture, PSR-6 caching, Twig integration, two console commands, Symfony Events, and a rich value-object API.

---

✅ Features
----------

[](#-features)

FeatureDescription🔥 **Bundled domain list**500+ domains ready to use out of the box🌐 **Remote sync**Pull 110K+ domains on demand via `php bin/console disposable-email:sync`🧩 **Extensible loaders**Tag any service with `disposable_email.domain_loader`🛡️ **Whitelist providers**Tag any service with `disposable_email.whitelist_provider`🏷️ **PHP 8 Attributes**`#[AsDomainLoader]` `#[AsWhitelistProvider]` `#[AsDomain]` `#[AsDomainList]` `#[AsWhitelistedDomain]`📦 **Inline config domains**Add domains directly in YAML without a file🧠 **Symfony Validator**`#[NotDisposableEmail]` PHP 8 Attribute + YAML support⚙️ **Rich service API**`check()` returns a full `CheckResult` value object🧁 **Twig helpers**Function, filter, and test for templates🔔 **Symfony Events**`DisposableEmailCheckedEvent` + `DomainListSyncedEvent`🗃️ **PSR-6 caching**Plug in any Symfony cache pool🐛 **Debug command**`disposable-email:debug` for runtime inspection📝 **PSR-3 logging**All operations are logged at appropriate levels✅ **Symfony 6.4 + 7.x**Tested on PHP 8.2+---

🚀 Installation
--------------

[](#-installation)

```
composer require nalabdou/disposable-email-bundle
```

Register the bundle in `config/bundles.php`:

```
return [
    // ...
    Nalabdou\DisposableEmailBundle\DisposableEmailBundle::class => ['all' => true],
];
```

That's it. Zero configuration required to get started.

---

⚙️ Configuration
----------------

[](#️-configuration)

Publish the example config:

```
cp vendor/nalabdou/disposable-email-bundle/config/packages/disposable_email.yaml config/packages/
```

Full reference (`config/packages/disposable_email.yaml`):

```
disposable_email:

    # Directory for custom .txt blacklists and remote sync output
    blacklist_directory: '%kernel.project_dir%/var/disposable'

    # Domains added directly in config (no file required)
    extra_domains:
        - mycompetitor-fake.com

    # Set false to skip the bundled list entirely
    use_bundled_list: true

    # Remote sources for the sync command
    remote_sources:
        - url: 'https://remote_source_for_disposable_email_list'
          timeout: 30

    # Domains that are NEVER flagged as disposable
    whitelist:
        - mycompany.com
        - staging.mycompany.com

    cache:
        enabled: false          # true = highly recommended in production
        pool: 'cache.app'       # any PSR-6 Symfony cache pool
        ttl: 86400              # 24 h
        key_prefix: 'disposable_email'

    # Dispatch events on every check (set false for max throughput)
    dispatch_events: true
```

---

⚙️ Usage
--------

[](#️-usage)

### 1. Validator Constraint — PHP 8 Attribute (recommended)

[](#1-validator-constraint--php-8-attribute-recommended)

```
use Nalabdou\DisposableEmailBundle\Constraint\NotDisposableEmail;
use Symfony\Component\Validator\Constraints as Assert;

class RegistrationDto
{
    #[Assert\NotBlank]
    #[Assert\Email]
    #[NotDisposableEmail]
    public string $email = '';
}
```

With a custom error message:

```
#[NotDisposableEmail(message: 'Please use a real, permanent email address.')]
public string $email = '';
```

### 2. Validator Constraint — YAML

[](#2-validator-constraint--yaml)

```
# config/validator/App.Entity.User.yaml
App\Entity\User:
    properties:
        email:
            - NotBlank: ~
            - Email: ~
            - Nalabdou\DisposableEmailBundle\Constraint\NotDisposableEmail: ~
```

### 3. Runtime Service — Simple API

[](#3-runtime-service--simple-api)

Inject `DisposableEmailChecker` anywhere via constructor injection:

```
use Nalabdou\DisposableEmailBundle\Service\DisposableEmailChecker;

class RegistrationHandler
{
    public function __construct(
        private readonly DisposableEmailChecker $checker,
    ) {}

    public function handle(string $email): void
    {
        if ($this->checker->isDisposable($email)) {
            throw new \DomainException('Disposable emails are not allowed.');
        }
    }
}
```

Accepts bare domains too:

```
$checker->isDisposable('mailinator.com');   // true
$checker->isValid('gmail.com');             // true
$checker->count();                          // number of loaded disposable domains
```

### 4. Runtime Service — Rich `CheckResult` API

[](#4-runtime-service--rich-checkresult-api)

```
$result = $checker->check('user@mailinator.com');

$result->isDisposable();   // bool
$result->isValid();        // bool
$result->isWhitelisted();  // bool
$result->domain;           // 'mailinator.com'
$result->detectedBy;       // 'bundled' — which loader flagged it
$result->fromCache;        // bool — was result served from PSR-6 cache?

echo $result;
// [DISPOSABLE] user@mailinator.com (domain: mailinator.com, cache: no, detected_by: bundled)
```

### 5. Twig

[](#5-twig)

```
{# Twig test (most readable) #}
{% if user.email is disposable_email %}
    ⚠ Disposable email detected.
{% else %}
    ✔ Email looks valid.
{% endif %}

{# Twig filter #}
{% if user.email|is_disposable_email %}
    Disposable
{% endif %}

{# Full CheckResult object via function #}
{% set result = disposable_email_check(user.email) %}
{% if result.disposable %}
    Flagged by loader: {{ result.detectedBy }}
{% endif %}
```

---

🔄 Console Commands
------------------

[](#-console-commands)

### `disposable-email:sync` — Update the domain list

[](#disposable-emailsync--update-the-domain-list)

```
# Sync all configured remote_sources
php bin/console disposable-email:sync

# Show a per-source table
php bin/console disposable-email:sync --stats
```

Schedule automatic sync with cron:

```
# /etc/cron.d/disposable-email
0 3 * * * www-data /var/www/html/bin/console disposable-email:sync >> /var/log/disposable-email-sync.log 2>&1

```

Or with the Symfony Scheduler:

```
use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\RecurringMessage;
use Symfony\Component\Scheduler\Schedule;
use Symfony\Component\Scheduler\ScheduleProviderInterface;

#[AsSchedule]
final class AppSchedule implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        return (new Schedule())->add(
            RecurringMessage::cron('0 3 * * *', new SyncDisposableEmailsMessage()),
        );
    }
}
```

### `disposable-email:debug` — Inspect runtime state

[](#disposable-emaildebug--inspect-runtime-state)

```
# Overview (loader count, total domains, etc.)
php bin/console disposable-email:debug

# Check a specific email or domain
php bin/console disposable-email:debug user@mailinator.com
php bin/console disposable-email:debug mailinator.com

# List all registered loaders and whitelist providers
php bin/console disposable-email:debug --loaders

# Search for domains containing a substring
php bin/console disposable-email:debug --search=mailinator

# Show total domain count
php bin/console disposable-email:debug --count
```

---

📝 Custom Blacklist
------------------

[](#-custom-blacklist)

Drop any `.txt` file (one domain per line) into the configured `blacklist_directory`:

```
# var/disposable/my_domains.txt
competitor-disposable.com
internal-test.local

```

No command needed. The file is picked up automatically on the next request (or cache refresh).

---

🏷️ PHP 8 Attributes
-------------------

[](#️-php-8-attributes)

All five bundle attributes are in the `Nalabdou\DisposableEmailBundle\Attribute\` namespace. They are the zero-YAML alternative to YAML tags and inline config — pure PHP, fully type-safe, discovered automatically at container compile time.

### `#[AsDomainLoader]` — Register a loader service

[](#asdomainloader--register-a-loader-service)

```
use Nalabdou\DisposableEmailBundle\Attribute\AsDomainLoader;
use Nalabdou\DisposableEmailBundle\Contract\DomainLoaderInterface;
use Doctrine\DBAL\Connection;

#[AsDomainLoader(priority: 20)]
final class DatabaseDomainLoader implements DomainLoaderInterface
{
    public function __construct(private readonly Connection $db) {}

    public function load(): iterable
    {
        return $this->db->fetchFirstColumn('SELECT domain FROM disposable_domains');
    }

    public function getName(): string { return 'database'; }
    public function isEnabled(): bool { return true; }
}
```

Override the name surfaced in `CheckResult::$detectedBy` and debug output:

```
#[AsDomainLoader(priority: 20, name: 'my_custom_source')]
final class DatabaseDomainLoader implements DomainLoaderInterface { ... }
```

**Priority reference:**

SourcePriorityBundled list`-10`Custom blacklist files`0``#[AsDomain]` / `#[AsDomainList]` attributes`5`Inline `extra_domains` YAML`10`Your `#[AsDomainLoader]` classes`20+` (recommended)---

### `#[AsWhitelistProvider]` — Register a whitelist provider service

[](#aswhitelistprovider--register-a-whitelist-provider-service)

```
use Nalabdou\DisposableEmailBundle\Attribute\AsWhitelistProvider;
use Nalabdou\DisposableEmailBundle\Contract\WhitelistProviderInterface;

#[AsWhitelistProvider]
final class CompanyWhitelistProvider implements WhitelistProviderInterface
{
    public function __construct(private readonly Connection $db) {}

    public function getWhitelistedDomains(): iterable
    {
        return $this->db->fetchFirstColumn('SELECT domain FROM trusted_domains');
    }
}
```

With an optional description visible in `disposable-email:debug --loaders`:

```
#[AsWhitelistProvider(description: 'Company-approved domains from CRM')]
final class CompanyWhitelistProvider implements WhitelistProviderInterface { ... }
```

---

### `#[AsDomain]` — Mark a constant or enum case as a disposable domain

[](#asdomain--mark-a-constant-or-enum-case-as-a-disposable-domain)

**On class constants:**

```
use Nalabdou\DisposableEmailBundle\Attribute\AsDomain;

final class KnownDisposableDomains
{
    #[AsDomain]
    public const MAILINATOR = 'mailinator.com';

    #[AsDomain]
    public const GUERRILLA = 'guerrillamail.com';

    // No attribute — not loaded
    public const SOME_INTERNAL = 'internal.local';
}
```

**On a string-backed enum (recommended for domain modelling):**

```
use Nalabdou\DisposableEmailBundle\Attribute\AsDomain;

enum DisposableDomain: string
{
    #[AsDomain]
    case Mailinator = 'mailinator.com';

    #[AsDomain]
    case Trashmail  = 'trashmail.com';
}
```

**With an explicit domain override:**

```
#[AsDomain(domain: 'actual-domain.com')]
public const LEGACY_CONSTANT_NAME = 'old-value.com';
// Loads 'actual-domain.com', not 'old-value.com'
```

Register your class as a service to have it discovered:

```
# config/services.yaml  (or use autowire: true on the whole App\ namespace)
App\Mail\KnownDisposableDomains:
    public: false
```

---

### `#[AsDomainList]` — Declare an inline or static-method domain list

[](#asdomainlist--declare-an-inline-or-static-method-domain-list)

Lighter than `#[AsDomainLoader]` — no interface required. Perfect for simple static lists that don't need lifecycle control.

**Inline domains:**

```
use Nalabdou\DisposableEmailBundle\Attribute\AsDomainList;

#[AsDomainList(
    domains: ['fakeinbox.com', 'tempmail.io', 'throwaway.email'],
    priority: 15,
)]
final class ProjectBlockList {}
```

**Via a static method:**

```
#[AsDomainList(method: 'getDomains', priority: 15, name: 'project_blocklist')]
final class ProjectBlockList
{
    public static function getDomains(): array
    {
        return ['fakeinbox.com', 'tempmail.io', 'throwaway.email'];
    }
}
```

**Multiple lists on one class** (`IS_REPEATABLE`):

```
#[AsDomainList(domains: ['competitor-a.com', 'competitor-b.com'], priority: 20)]
#[AsDomainList(method: 'getRegionalDomains', priority: 15)]
final class AllBlockLists
{
    public static function getRegionalDomains(): array { ... }
}
```

The `method` must be `static`. The compiler pass validates this and throws a `\LogicException` at compile time if the method is missing or non-static.

---

### `#[AsWhitelistedDomain]` — Mark a constant or enum case as a trusted domain

[](#aswhitelisteddomain--mark-a-constant-or-enum-case-as-a-trusted-domain)

**On class constants:**

```
use Nalabdou\DisposableEmailBundle\Attribute\AsWhitelistedDomain;

final class TrustedDomains
{
    #[AsWhitelistedDomain]
    public const COMPANY = 'mycompany.com';

    #[AsWhitelistedDomain]
    public const STAGING = 'staging.mycompany.com';

    // No attribute — not whitelisted
    public const PARTNER_LEGACY = 'old-partner.com';
}
```

**On a string-backed enum:**

```
use Nalabdou\DisposableEmailBundle\Attribute\AsWhitelistedDomain;

enum TrustedDomain: string
{
    #[AsWhitelistedDomain]
    case Company = 'mycompany.com';

    #[AsWhitelistedDomain]
    case Staging = 'staging.mycompany.com';
}
```

**With an explicit domain override:**

```
#[AsWhitelistedDomain(domain: 'real-domain.com')]
public const PLACEHOLDER = 'draft-name.com';
```

---

### Attribute quick-reference

[](#attribute-quick-reference)

AttributeTargetPurpose`#[AsDomainLoader]`ClassRegister a `DomainLoaderInterface` service`#[AsWhitelistProvider]`ClassRegister a `WhitelistProviderInterface` service`#[AsDomain]`Class constant / enum caseMark a single value as a disposable domain`#[AsDomainList]`ClassDeclare an inline or static-method domain list`#[AsWhitelistedDomain]`Class constant / enum caseMark a single value as a trusted (whitelisted) domain---

🧩 Extending — Custom Domain Loader
----------------------------------

[](#-extending--custom-domain-loader)

Implement `DomainLoaderInterface` and tag the service:

```
namespace App\Mail;

use Nalabdou\DisposableEmailBundle\Contract\DomainLoaderInterface;
use Doctrine\DBAL\Connection;

final class DatabaseDomainLoader implements DomainLoaderInterface
{
    public function __construct(private readonly Connection $db) {}

    public function load(): iterable
    {
        return $this->db->fetchFirstColumn('SELECT domain FROM disposable_domains');
    }

    public function getName(): string { return 'database'; }
    public function isEnabled(): bool { return true; }
}
```

```
# config/services.yaml
App\Mail\DatabaseDomainLoader:
    tags:
        - { name: disposable_email.domain_loader, priority: 20 }
```

Note: when two loaders provide the same domain, the higher-priority one is credited in `CheckResult::$detectedBy`.

---

🛡️ Extending — Custom Whitelist Provider
----------------------------------------

[](#️-extending--custom-whitelist-provider)

```
namespace App\Mail;

use Nalabdou\DisposableEmailBundle\Contract\WhitelistProviderInterface;

final class DatabaseWhitelistProvider implements WhitelistProviderInterface
{
    public function __construct(private readonly Connection $db) {}

    public function getWhitelistedDomains(): iterable
    {
        return $this->db->fetchFirstColumn('SELECT domain FROM trusted_domains');
    }
}
```

```
App\Mail\DatabaseWhitelistProvider:
    tags:
        - { name: disposable_email.whitelist_provider }
```

---

🔔 Events
--------

[](#-events)

### `DisposableEmailCheckedEvent` (on every check)

[](#disposableemailcheckedevent-on-every-check)

```
use Nalabdou\DisposableEmailBundle\Event\DisposableEmailCheckedEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener(event: DisposableEmailCheckedEvent::NAME)]
final class DisposableEmailListener
{
    public function __invoke(DisposableEmailCheckedEvent $event): void
    {
        $result = $event->getResult();

        if ($result->isDisposable()) {
            $this->logger->warning('Disposable email attempt', [
                'email'       => $result->input,
                'domain'      => $result->domain,
                'detected_by' => $result->detectedBy,
            ]);
        }
    }
}
```

### `DomainListSyncedEvent` (after sync command)

[](#domainlistsyncedevent-after-sync-command)

```
use Nalabdou\DisposableEmailBundle\Event\DomainListSyncedEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener(event: DomainListSyncedEvent::NAME)]
final class SyncCompletedListener
{
    public function __invoke(DomainListSyncedEvent $event): void
    {
        if ($event->hasFailures()) {
            $this->notifyOpsChannel('Disposable email sync had failures!');
        }

        $this->metrics->gauge('disposable_email.total_domains', $event->getTotalDomains());
    }
}
```

---

🧠 Caching (Production)
----------------------

[](#-caching-production)

Strongly recommended in production to avoid rebuilding the 100K+ domain set on every request:

```
# config/packages/disposable_email.yaml
disposable_email:
    cache:
        enabled: true
        pool: 'cache.app'   # or 'cache.redis', any PSR-6 pool
        ttl: 86400
```

After syncing a new domain list, the cache is invalidated automatically. To clear it manually:

```
php bin/console cache:pool:clear cache.app
```

---

📁 Directory Structure
---------------------

[](#-directory-structure)

```
DisposableEmailBundle/
├── composer.json
├── phpunit.xml.dist
├── README.md
│
├── config/
│   ├── services.php
│   └── packages/
│       └── disposable_email.yaml
│
├── resources/
│   └── domains/
│       └── disposable_domains.txt
│
└── src/
    ├── DisposableEmailBundle.php
    │
    ├── Attribute/
    │   ├── AsDomain.php
    │   ├── AsDomainList.php
    │   ├── AsDomainLoader.php
    │   ├── AsWhitelistedDomain.php
    │   └── AsWhitelistProvider.php
    │
    ├── Command/
    │   ├── SyncDisposableEmailListCommand.php
    │   └── DebugDisposableEmailCommand.php
    │
    ├── Constraint/
    │   ├── NotDisposableEmail.php
    │   └── NotDisposableEmailValidator.php
    │
    ├── Contract/
    │   ├── CheckResult.php
    │   ├── DomainLoaderInterface.php
    │   ├── SyncResult.php
    │   └── WhitelistProviderInterface.php
    │
    ├── DependencyInjection/
    │   ├── Compiler/
    │   │   ├── DomainLoaderPass.php
    │   │   ├── RegisterAttributesPass.php
    │   │   └── WhitelistProviderPass.php
    │   ├── Configuration.php
    │   └── DisposableEmailExtension.php
    │
    ├── Event/
    │   ├── DisposableEmailCheckedEvent.php
    │   └── DomainListSyncedEvent.php
    │
    ├── Exception/
    │   ├── DisposableEmailException.php
    │   ├── DomainLoaderException.php
    │   └── SyncException.php
    │
    ├── Loader/
    │   ├── AbstractFileLoader.php
    │   ├── AttributeDomainLoader.php
    │   ├── BundledDomainLoader.php
    │   ├── ChainDomainLoader.php
    │   ├── CustomBlacklistLoader.php
    │   └── InlineDomainsLoader.php
    │
    ├── Provider/
    │   ├── AttributeWhitelistProvider.php
    │   ├── ChainWhitelistProvider.php
    │   └── ConfigWhitelistProvider.php
    │
    ├── Service/
    │   ├── DisposableEmailChecker.php
    │   └── DomainListSyncer.php
    │
    └── Twig/
        └── DisposableEmailExtension.php

tests/
    ├── Attribute/
    │   └── AttributesTest.php
    ├── Constraint/
    │   └── NotDisposableEmailValidatorTest.php
    ├── DependencyInjection/
    │   └── RegisterAttributesPassTest.php
    └── Service/
        ├── ChainDomainLoaderTest.php
        ├── DisposableEmailCheckerTest.php
        ├── LoadersTest.php
        ├── ValueObjectsTest.php
        └── WhitelistProviderTest.php

```

---

Running Tests
-------------

[](#running-tests)

```
composer install
vendor/bin/phpunit
```

---

License
-------

[](#license)

This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance87

Actively maintained with recent releases

Popularity1

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

64d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/e872d64f192da649961f598268ebc6a7622495868821b602fee29233ec58c70b?d=identicon)[nalabdou](/maintainers/nalabdou)

---

Top Contributors

[![nalabdou](https://avatars.githubusercontent.com/u/46465503?v=4)](https://github.com/nalabdou "nalabdou (13 commits)")

---

Tags

disposable-emailemailsymfonysymfony-bundlesymfony6symfonybundlevalidationemailblacklistdisposableemail validationtemporary email

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/nalabdou-disposable-email-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/nalabdou-disposable-email-bundle/health.svg)](https://phpackages.com/packages/nalabdou-disposable-email-bundle)
```

###  Alternatives

[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.4M195](/packages/sulu-sulu)[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.5M370](/packages/easycorp-easyadmin-bundle)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.4M506](/packages/shopware-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
