azine/mailgunwebhooks-bundle
Symfony bundle that captures Mailgun webhook event data in your database, so you can search, filter, and display delivery logs beyond Mailgun's 48-hour retention, delete old events via CLI, and get alerts for blacklisted sending IPs or spam-related bounces.
Architecture fit: fundamental — and disqualifying — mismatch. Despite the brief describing this as a "Laravel/PHP package," azine/mailgunwebhooks-bundle is a Symfony bundle, not a Composer package with framework-agnostic or Laravel-native wiring. There is no service provider, no publishable config, no publishable migrations, and no Blade equivalents. The integration contract is Symfony-specific end to end:
| Concern | What the bundle ships | Laravel equivalent needed |
|---|---|---|
| Registration | AppKernel::registerBundles() / bundles.php |
config/app.php providers |
| HTTP endpoint | YAML routing resources (mailgunevent_webhook.yml, mailgunevent_admin.yml) |
routes/api.php, routes/web.php |
| Config | config.yml tree with a bundle Configuration class |
config/*.php + config() |
| Persistence | Doctrine ORM 3 entities + repositories | Eloquent models/migrations |
| Templating | Twig 3 | Blade |
| CLI | Symfony Console commands (mailgun:delete-events, mailgun:check-ip-in-blacklist) |
Artisan commands |
| Extension points | Symfony EventDispatcher (MailgunWebhookEvent) |
Laravel events/listeners |
The value in this repo is not the wiring — it's the domain logic: webhook signature validation, replay/deduplication, the event schema, retention pruning, and the blacklist/IP-health check. That logic is portable; the DI, routing, and ORM layers are not. A TPM should treat this as a reference implementation to be cannibalized, not a dependency to be installed.
Integration feasibility: low for drop-in, medium for reimplementation. Attempting to consume this in a Laravel app would require standing up Doctrine ORM 3 alongside Eloquent, bridging Symfony's HttpKernel/EventDispatcher, and shimming the bundle extension — a stack-of-cards that we would own forever for ~10 stars of upstream support. Realistically our own webhook receiver (controller + queued job + Eloquent mailgun_events table + signed-URL admin view) is a 1–2 engineer-week build and yields ~90% of the feature surface with zero cross-framework tax.
Technical risk: high, on multiple independent axes.
AppKernel.php, app/config/config.yml, app/console, and Symfony 2.x router:debug, while the release notes describe Symfony 7.4 DI. That gap signals the docs are not exercised against the current code — a classic source of multi-day integration surprises.[email protected] and "MuBu Properties Mailer" — a third party's production values. Any copy-paste onboarding risks sending mail attributed to an unrelated domain./admin/. Signature validation and "safe error responses" are a 5.0.0 improvement, but a self-hosted, low-scrutiny project owning an internet-facing write endpoint is a finding in any security review we run.-date '60 days ago' and -numberOfAttempts 5 use single-dash long options. Fine in Symfony Console; a friction point if ever ported to Artisan.Key questions to resolve before any commitment:
Stack fit: poor as a dependency, good as a spec. The bundle assumes a full Symfony application container. In a Laravel codebase it is a foreign body. The pragmatic reading is that this repo is a well-documented requirements document for a capability we should build natively.
Migration path — three options, ranked:
Option A (recommended): Reimplement natively in Laravel. Build a small first-party module:
POST /webhooks/mailgun route with HMAC signature verification (Mailgun-Signature/Timestamp/Token) and a constant-time compare.mailgun_events table keyed on a natural dedup key (Mailgun event ID / signature token + timestamp) so retries are idempotent.ShouldQueue) so Mailgun's retry budget is never spent waiting on our DB.mailgun:prune --before="60 days ago" and mailgun:check-ip./admin/ surface.Option B: Sidecar Symfony service. Deploy the bundle as an isolated Symfony app that owns only the webhook receiver and event store, exposing a read API to the Laravel app. Viable if we already operate Symfony services or want strict blast-radius isolation for a public endpoint. Cost: a second runtime, a second deploy pipeline, a second security review, and a shared-database or API contract to maintain. Justified only if Option A's timeline is unattainable.
Option C (not recommended): Fork and shim. Replace Doctrine with Eloquent and Symfony wiring with a Laravel service provider. We would inherit the domain logic but also the maintenance burden of a hard fork with no upstream merge path (upstream is on Symfony 7.4/Doctrine 3 by design). Rejected: high effort, permanently orphaned.
Compatibility notes if any option carries upstream code forward:
Sequencing (assuming Option A):
Maintenance. Option A puts a small, conventional Laravel module in our own codebase — reviewed, tested, and upgraded on our cadence. Option B inherits the upstream project's release rhythm: PHP 8.5 and Symfony 7.4 arrive when the author says so, with zero dependents to absorb the breakage first. Option C inherits the worst of both. The 5.0.0 release notes are genuinely encouraging (strict signature validation, dedup, bounded observable retries), but a forward-dated release timestamp and a "readme"-level maturity signal mean we should not assume a support relationship exists. Assume no upstream support in any scenario.
Support. No vendor, no SLA, no community of size. Escalation path is "read the source." This is acceptable for a 1,500-line internal module we wrote; it is not acceptable for an internet-facing component whose failure mode is silent loss of deliverability alerts. Budget internal ownership explicitly — a named engineer, not "the platform team."
Scaling. Volumes are modest by design (the README's framing is sub-10k emails/month). The real scaling pressure is storage, not throughput: every delivery, open, click, bounce, and complaint becomes a row, and open/click events multiply fastest. Two levers:
Failure modes to design for (each needs an explicit mitigation, not a hope):
| Failure mode | Blast radius | Mitigation |
|---|---|---|
| Alert email to admin silently fails | Deliverability incident goes unseen | Route alerts through existing observability/on-call, not raw SMTP; alert on the alerter |
| Cron pruner stops running | Disk fill → DB degradation or outage | Monitor scheduler liveness; alert on table size growth |
| Scheduler misconfigured after deploy | Silent retention breach or premature data loss | Assert row-count/age bounds in a health check |
| Webhook endpoint flooded by scanners/forged posts | Storage exhaustion, noisy metrics | Signature verification (non-negotiable), rate limiting, WAF rule, reject-and-log at edge |
| Mailgun retry storm after partial outage | Duplicate rows, inflated engagement metrics | Unique dedup key at DB level, not application-level check-then-insert |
| HetrixTools quota or API outage | Blacklist monitoring blind | Treat check as best-effort; alert on consecutive failures rather than assuming "no listings" |
| Admin UI exposed without auth | Event data (recipient addresses, subjects) leaks | Never use the bundle's /admin/ prefix as-is; wrap in existing authz |
| Upstream stops publishing | Version drift, security patches stop | Accepted only under Option A, where we own the code |
Ramp-up. Deceptively steep. The README interleaves Symfony 2.x-era instructions (app/console, AppKernel, app/config) with 5.0.0's Symfony 7.4 claims, so an engineer following it literally will fail before reaching the interesting parts. Anyone onboarding must first determine which era the docs describe. For a TPM this is a warning sign about the project's internal consistency, and it argues against handing this to a junior engineer as a "just install it" task. Under Option A, ramp-up is essentially zero — the code matches the rest of our application.
Bottom line for the decision log: adopt the requirements, not the dependency. The 5.0.0 hardening (signature validation, replay/dedup, bounded observable retries) is exactly the right feature set and validates the need; the Symfony-only packaging and PHP 8.5 floor make it the wrong artifact for a Laravel estate. Recommend a native Laravel build, with the bundle retained as the reference spec and the sidecar deployment held as a fallback only if the delivery timeline collapses.
How can I help you explore Laravel packages today?