bogardo/mailgun
A Laravel package that integrates Mailgun into your app, providing a Mailgun transport driver for Laravel's Mailer plus a simple API wrapper for sending messages and managing mailing lists, routes, and webhooks.
The package is a thin Laravel wrapper around the official mailgun/mailgun-php v2 SDK that adds Blade rendering and a Mailgun facade. It is archived (last release October 2019), so treat it as maintenance-mode tooling for existing applications rather than a greenfield choice.
Minimal installation:
composer require bogardo/mailgun
Bogardo\Mailgun\MailgunServiceProvider and the Mailgun facade alias.config/app.php and the alias 'Mailgun' => Bogardo\Mailgun\Facades\Mailgun::class.Publish and configure:
php artisan vendor:publish --provider="Bogardo\Mailgun\MailgunServiceProvider"
MAILGUN_DOMAIN=mg.example.com
MAILGUN_SECRET=key-xxxxxxxxxxxxxxxxxxxxxxxx
config/mailgun.php holds domain, secret, endpoint (defaults to api.mailgun.net), a default from address/name, and views roots used by the view-rendering helpers.
First send:
use Mailgun;
Mailgun::send('emails.welcome', ['user' => $user], function ($message) use ($user) {
$message->to($user->email, $user->name)
->subject('Welcome aboard')
->from('[email protected]', 'Example');
});
Where to look first, in order:
config/mailgun.php — it tells you exactly which env keys and view roots are in play.bogardo/mailgun — it has the full facade method list and the Message builder options.src/Bogardo/Mailgun/Mailgun.php and Message.php in vendor/ — the callback object is where tags, tracking, attachments, and recipient variables live.composer.lock — confirm which mailgun/mailgun-php major version resolved; it drives most compatibility issues.The canonical first use case is transactional mail (welcome, password reset, receipts) where you already have Blade templates and want Mailgun's delivery/tracking without writing a transport.
View-based sends. The views config lets you map logical names to directories so Mailgun::send('welcome', $data, $cb) renders resources/views/emails/html/welcome.blade.php (and a matching text variant if configured). This is the most common pattern — keep all mail bodies in Blade and pass a plain array of data.
HTML + text pairs. Configure both views.html and views.text; the package will render both parts for the same logical view name. If you only need plain text, use the raw/text helper instead of a view.
Attachments and inline images. Inside the callback:
$message->attach(storage_path('app/invoices/'.$invoice->id.'.pdf'), 'invoice.pdf');
$message->embed(storage_path('app/logo.png')); // used as cid: reference in the HTML
Paths are filesystem paths, not URLs.
Message options. The callback object mirrors Mailgun's o: parameters — typically tag(), tracking(), dkim(), campaign(), and scheduled delivery. Reach for these before dropping to the raw SDK.
Batch sends with recipient variables. For newsletters or per-user personalization across many recipients, use the batch helper rather than looping the facade — one HTTP call instead of N, and Mailgun will substitute %recipient.first_name% tokens:
Mailgun::sendBatch('emails.newsletter', $recipients, function ($message, $recipient) {
$message->to($recipient['email'])
->subject('Your weekly digest')
->recipientVariables(['first_name' => $recipient['first_name']]);
});
Check the README for the exact per-recipient argument shape in the version you have pinned.
Address validation. Use the facade's validation helper before persisting signups; it calls Mailgun's validation endpoint and saves you a round of bounces.
Wrap it, don't scatter it. Because this bypasses Laravel's Mail facade entirely, put calls behind a small App\Mail\MailgunSender service (or a queued Job) so tests and future migration to the built-in Mailgun transport touch one file. Dispatch an SendMailgunMessage job for anything user-triggered — facade calls are synchronous HTTP.
Testing. Mock the facade in feature tests:
Mailgun::shouldReceive('send')->once()->withArgs(fn ($view) => $view === 'emails.welcome');
Call Mailgun::clearResolvedInstances() in tearDown if the facade caches between tests. For unit tests, bind a fake implementation to Bogardo\Mailgun\Contracts\Mailgun.
Multi-domain / multi-tenant. The config is resolved once at boot, so runtime domain switching means config(['mailgun.domain' => $tenant->mailgun_domain]) before the call, or instantiating the underlying SDK directly for a cleaner separation.
MAIL_MAILER=mailgun + symfony/mailgun-mailer), and Laravel 9+ supports Mailgun via Symfony's bridge. New code should use that; this package is only justified if you depend on its Blade-view sugar or batch helpers.mailgun/mailgun-php v2. If Composer pulls v3/v4, the API surface changes (client construction, exceptions, method signatures) and you get fatals at runtime. Pin mailgun/mailgun-php:^2 in your root composer.json and verify after every composer update.'endpoint' => 'api.eu.mailgun.net' for EU-region domains; the default US endpoint returns 401/404 for them. The secret is region-scoped.key-…). Accidentally prefixing api: (or stripping it) is a classic 401 source; behavior differs between SDK patch versions.php artisan config:cache freezes mailgun.domain/secret. Keep env() calls only inside config/mailgun.php, never at call sites.Mail::to()->send(new Welcome). Either call the facade from inside the Mailable's build(), or implement a custom notification channel that delegates to your wrapper service.Mailgun\Exception\HttpClientException (400/401/404) and HttpServerException (5xx); log getResponseBody() — Mailgun's error JSON names the offending parameter.embed().Mailgun aliases fight over the root. Check config/app.php aliases and composer show | grep mailgun.$message is not available inside the view; everything the template needs must be in the data array you pass to send().Bogardo\Mailgun\Contracts\Mailgun in a service provider to swap in a decorator (logging, dry-run mode, tenant routing) without touching call sites — the facade resolves through the container.How can I help you explore Laravel packages today?