emailsherlock/email-guard-core
PHP guard for signup and checkout email fields: offline checks catch bad syntax, reserved TLDs, and 73k+ disposable domains with no network calls. Add an EmailSherlock API key for live MX, SMTP, role and catch-all detection. PHP 8.1+.
Architecture fit
emailsherlock/email-guard-core is a deliberately thin, framework-agnostic library: a single EmailGuard entry point returning a Result object with an action enum (Allow / Deny / Review) plus a reasons collection. That shape is the right one for a Laravel codebase — it is pure PHP with no HTTP kernel coupling, no global state, and no framework dependency, so it can be bound as a container singleton and invoked from a FormRequest, a custom validation rule, a controller, or a queued job without architectural compromise.
The local-first / API-escalation split is the strongest architectural property. Default behavior is deterministic, offline, and zero-latency (syntax, reserved TLDs, 73k+ bundled disposable domains). Escalation to the Verify API is opt-in via api_key and is policy-driven (block_on, review_on). That means the package can be adopted without a vendor contract, without a network dependency on the hot path of signup, and without a data-processing agreement in place on day one — which materially lowers the approval cost of a trial.
The Allow / Deny / Review triad is also a better fit for real product surfaces than a boolean. Review maps cleanly to a hold-for-confirmation state (catch-all domains, role accounts) rather than forcing a binary reject decision into the validation layer.
Integration feasibility
Composer-installable, MIT licensed, PHP 8.1+. No Laravel service provider or facade is published, so integration means writing a small first-party wrapper: a config/email-guard.php, a provider that binds EmailGuard with config-derived options, and a Rule object that maps Deny to a validation failure and Review to a flagged attribute. That is roughly half a day of work for a mid-level engineer and is genuinely low-risk because the surface area is one method.
The real feasibility question is not "can we wire it up" but "do we want to own the wiring." The README states the Symfony bundle and WordPress plugin build on this library, which means the maintainer intends the core to be the stable substrate and framework adapters to be separate artifacts. A Laravel adapter may therefore appear upstream later — worth asking, because writing our own now creates a small migration liability if an official adapter lands with a different config schema.
Technical risk
| Risk | Severity | Notes |
|---|---|---|
| Maturity signals | High | 0 stars, 0 dependents, single maintainer. No community validation of the code or the API contract. |
| Release provenance | High | Last release dated 2026-06-19 — a future date. Either a clock/metadata error or a pre-dated tag. Treat as unverified until the tag, commit SHA, and Packagist entry are reconciled. |
| Bus factor | High | Single-vendor, effectively single-person. No evidence of a contributing cohort. |
| Bundled snapshot staleness | Medium | The 73k disposable-domain list is only as good as its refresh cadence. Disposable domains rotate fast; a stale snapshot silently degrades to allow. |
| Policy misconfiguration | Medium | Aggressive block_on (catch-all, role) will reject legitimate corporate and shared-inbox signups. This is a product-conversion risk, not just a technical one. |
| PII egress | Medium | Escalation sends the email address to a third party. Requires DPA/privacy review and a documented lawful basis before enabling in EU-facing flows. |
| Supply chain | Medium | Unknown transitive dependency tree. A zero-star package with a small maintainer base is a plausible typosquat/compromise target — pin by SHA and review the dependency graph. |
| Test coverage | Unknown | No CI or coverage badge in the excerpt. Assume unverified until inspected. |
Maturity caveats worth stating plainly to stakeholders: the package advertises readme, changelog, releases as its maturity level, which is the floor of credible packaging, not evidence of production hardening. Score of ~13.9 with opportunity at ~56 suggests a package with a real problem thesis but no adoption. Recommend it be evaluated as a candidate, not a commitment.
Key questions
^0.x safe, or do we need exact pins?Stack fit
| Laravel surface | Fit | Notes |
|---|---|---|
Custom validation Rule |
Excellent | Deny → fail with reasons; Review → pass + flash/flag. |
FormRequest |
Excellent | Drop-in via the rule. |
| Service provider binding | Good | Trivial; ~20 lines. |
config/email-guard.php |
Good | Keep api_key in env, never in config cache output. |
| Queued job (async escalation) | Good | Review decisions can be resolved post-signup without blocking the request cycle. |
| Event / listener | Good | Emit EmailDenied / EmailHeldForReview for analytics and abuse dashboards. |
| Laravel Nova / Filament admin | Neutral | Manual — render reasons on the user record. |
The package does not attempt to own persistence or the user model. That is correct for a Laravel integration: store the action and reasons on the user or a side table, not inside the library.
Migration path
Adopt in three gates, each reversible:
Deny decisions against actual downstream behavior (did the account verify email? did it convert? did it chargeback?). This produces the false-positive evidence needed to justify a policy, and it costs nothing because local checks are offline..invalid address is not a lost customer. Measure signup completion delta.Review on catch-all. Add the API key behind a feature flag, scoped to a percentage of traffic. Review rather than Deny for catch-all preserves conversion while collecting signal.Do not run gates 2 and 3 simultaneously. Attribution becomes impossible.
Compatibility
__DIR__, not a path relative to the vendor root (a classic break under optimized autoloaders or when the package is symlinked in local development).config:cache and route:cache — configuration must be read at boot, not at call time.Sequencing
Allow).Maintenance
The recurring cost is not the code — it is the snapshot. The disposable-domain list is the package's core value and its core liability: it decays continuously and the refresh mechanism is undocumented in the excerpt. Budget a quarterly review to confirm the pinned version has a recent snapshot, and treat "snapshot older than 90 days" as a maintenance ticket. Composer pinning should be exact or tightly bounded until SemVer discipline is confirmed; a ^0.x range on a zero-adoption package is an uncontrolled upgrade path on your signup form.
We also own the wrapper, which means we own the config schema, the validation rule, the event contract, and the tests. That is a small but permanent surface — roughly a day per quarter of upkeep, plus the migration cost if an official Laravel adapter later lands with a different shape.
Support
There is no support contract for the library. Self-hosted local checks are fully within our control and diagnosable from source; the escalation path is not. The Verify API's SLA, status page, and incident communication are unknown and must be established before gate 3. Internally, define a single owner (backend platform, not the signup team) so that a policy change has one approver.
Support load will be dominated by false-positive complaints — "your site won't let me sign up with my work email" — routed through customer support with no visibility into why. Mitigation: log action and reasons to an internal-only admin field, and give support a documented override. Without this, the guard becomes a support-cost center rather than a fraud reduction.
Scaling
Local-only mode scales for free: it is an in-memory set lookup against a bundled snapshot, no I/O, no shared state, safe to invoke per-request and safe to fan out across any number of PHP-FPM workers. Memory overhead is the snapshot itself — measure it; a 73k-entry list is small but non-zero per worker under a large FPM pool, and it is loaded per process.
API escalation is the only scaling constraint. It converts a sub-millisecond validation into a network round trip on the signup critical path, subject to third-party rate limits and latency. Three mitigations, in order of preference: (a) restrict escalation to addresses that pass local checks and are not obviously free-mail, reducing call volume substantially; (b) cache decisions keyed by a hash of the normalized address, with a TTL long enough to absorb retries but short enough to catch new disposables; (c) move escalation off the request path entirely — accept, then verify asynchronously and flag the account, which is what the Review action was designed for.
Failure modes
| Failure | Effect if unhandled | Mitigation |
|---|---|---|
| Verify API timeout | Signup latency spike or exception on the hot path | Hard timeout (~1s), fail-open to local result only, circuit breaker |
| API 429 / quota exhausted | Silent degradation or hard failures | Quota alerts at 70%, fall back to local-only, never hard-fail signup |
| Invalid/expired API key | Same as above, but persistent and quiet | Startup health check; alert on elevated escalation error rate |
| Snapshot file missing or unreadable | Local checks silently pass everything | Fail loudly at boot — this is a configuration error, not a runtime one |
| Stale snapshot | Disposable signups leak through; no signal | Track deny-rate trend; alert on a sustained drop |
| Over-aggressive policy | Legitimate customers rejected at signup | Shadow-mode calibration, Review over Deny for catch-all, support override |
| Malformed/oversized email input | Package exception, 500 on a public form | Validate length/encoding before invoking; wrap in try/catch |
| Vendor abandonment | No snapshot updates, no security patches | Fork-and-vendor contingency; the local-check logic is small enough to reimplement in ~200 lines if needed |
The most important operational property: no failure mode of this package should ever prevent a legitimate customer from signing up. Everything above is expressible as a single rule — degrade to Allow when uncertain — and that rule should be written into the config defaults, not left to per-call-site discipline.
Ramp-up
Low, but not zero. A backend engineer unfamiliar with the package needs roughly half a day to read the source, wire the provider, write the rule, and cover it with tests; the API is one method and one result object. The larger ramp-up cost is organizational rather than technical:
Realistic timeline from approval to full enforcement: three to five weeks, of which engineering is under one. Plan and staff accordingly — the gating work is policy and privacy, and the technical integration is the fast part.
How can I help you explore Laravel packages today?