emailsherlock/email-guard-bundle
Symfony bundle that blocks junk and disposable email addresses at form submit using local checks, before they reach your database. Optional EmailSherlock API key adds live MX and SMTP verification.
Architecture fit — the critical finding first. This is a Symfony bundle, not a Laravel package. Its two public surfaces are (1) a VerifiedEmail constraint consumed by Symfony's Validator component, and (2) a verify_email form option that extends Symfony Form's EmailType. Laravel has neither — it has its own Validator and no first-party Form component. On a Laravel stack the bundle itself is largely dead weight: Flex auto-registration, config/bundles.php, and the form extension are all inert. What is genuinely reusable is the engine underneath, emailsherlock/email-guard-core-php, plus the policy semantics the bundle encodes (fail_open, block_on, review_on, timeout_ms).
The useful architectural split is local vs. remote. Local checks — syntax profile, reserved TLDs, 73k+ disposable domains — are deterministic, offline, zero-latency, and involve no PII egress. That layer is the durable value. The API key only adds MX lookup, SMTP probe, and list freshness.
Integration feasibility. High, but via a thin adapter rather than the bundle. Scope: one ValidationRule implementation, a service provider binding a configured client singleton, a config file, and a cache decorator. Roughly 1–2 engineering days including tests. The expensive part is policy, not code.
Technical risk.
fail_open: true that degradation is silent.Key questions.
composer.json PHP constraint; does email-guard-core-php run without Symfony components installed?block_on / config keys break?Stack fit. For Laravel 10/11/12, implement Illuminate\Contracts\Validation\ValidationRule so 'email' => ['required', 'email', new VerifiedEmail] works inside FormRequests; add a Validator::extend alias if string rules are preferred for DX. Do not attempt to boot the bundle's Symfony DI — you will spend more time fighting container bridging than writing the rule.
Recommended path: depend on email-guard-core-php directly and re-encode the bundle's policy defaults as your own config. If you want the bundle's ergonomics, fork it — MIT permits it — but accept that you are maintaining a fork of a zero-adoption package.
Migration path — three phases.
block_on: ['invalid'] only; route disposable to review_on or a soft warning banner rather than a hard fail.Compatibility. Mirror the YAML keys 1:1 in config/email-guard.php (fail_open, timeout_ms, block_on, review_on) with EMAILGUARD_API_KEY via env(), so upstream docs remain translatable for ops. Bind the client as a singleton; set explicit connect/read timeouts and retries = 0 — this is a gate, not a data fetch, and retrying inside a signup request is worse than failing open.
Sequencing. Adapter + unit tests → shadow instrumentation → cache layer → enforce local → enable API → async probe. Enabling the API before the cache is the single most common way this becomes both slow and expensive.
Maintenance. Small code surface: one rule, one provider, one config file. The real ongoing cost is the disposable-domain dataset — local-only means you own refreshing it or accepting drift. Track email-guard-core-php release cadence; a zero-star project has no downstream pressure to stay current. Budget roughly one day per quarter, plus contingency for a fork if the maintainer goes quiet.
Support. No community, no dependents, no Stack Overflow corpus. Escalation is GitHub issues to one author. The cheapest insurance available is a GuardInterface with a NullGuard and a competitor implementation behind it — then swapping to Kickbox, ZeroBounce, or plain validation never touches call sites.
Scaling. Local checks are in-process and free; they scale linearly and add negligible cost. The API path scales as both dollars and latency: N submissions → N calls unless cached. Cache by domain to absorb the bulk (disposable domains repeat heavily) and by address hash to absorb retries. Note that signup endpoints are pre-auth, so a bot flood turns the guard into a cost amplifier — rate-limit the guard itself, independently of the API's own limits. Multi-region deploys will have inconsistent check depth wherever port 25 is blocked.
Failure modes.
| Failure | Behavior | Mitigation |
|---|---|---|
| API down / timeout | fail_open → junk passes |
Alert on fail-open rate; set an SLO ceiling |
| Quota exhausted (429) | Likely fails open | Spend alert; cache; auto-downgrade to local |
| Port 25 blocked | SMTP probe silently no-ops | Detect at deploy; don't promise the check internally |
| Stale disposable list | False negatives | Refresh job, or accept and rely on API layer |
| False positive | Real user blocked | Route via review_on; never hard-fail signup |
Config typo in block_on |
Silently blocks nothing or everything | Schema-validate config; add an integration test asserting a known-bad address is rejected |
Ramp-up. One engineer, half a day on the spec and core library, one to two days to a tested Laravel rule. The expensive knowledge is not code: which categories to block, what false-positive budget the business tolerates, who owns the review queue, and whether legal accepts third-party PII egress on the signup path. Settle those before writing the adapter — they determine whether this package is the right shape at all. Given the Symfony-only surface and near-zero adoption, the defensible recommendation is: adopt the core library and the policy model, not the bundle.
How can I help you explore Laravel packages today?