mailkube / mailkube-laravel
Laravel mail transport for mailkube.
Requires
- php: >=8.3
- composer-runtime-api: ^2.2
- illuminate/mail: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
- mailkube/mailkube-php: ^1.1.0
- symfony/mailer: ^7.0 || ^8.0
- symfony/mime: ^7.0 || ^8.0
Requires (Dev)
- dealerdirect/phpcodesniffer-composer-installer: ^1.0
- friendsofphp/php-cs-fixer: ^3.64
- larastan/larastan: ^3.0
- laravel/framework: ^12.61.1 || ^13.12.0
- nyholm/psr7: ^1.8
- orchestra/testbench: ^10.0 || ^11.0
- php-http/mock-client: ^1.6
- phpmd/phpmd: ^2.15
- phpstan/phpstan: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^11.0
- slevomat/coding-standard: ^8.15
- squizlabs/php_codesniffer: ^3.10
This package is auto-updated.
Last update: 2026-08-19 18:34:52 UTC
README
Laravel mail transport for mailkube.
Send mail through mailkube using Laravel's own Mail facade, and receive webhooks as Laravel
events. This package is a thin adapter over
mailkube/mailkube-php: the API, retries, errors and
signature verification all live there.
Requirements
PHP 8.3+ and Laravel 12 or 13.
Laravel 11 is not supported. It is past security support, and the advisory covering
>=11.0.0,<12.0.0 has no patched 11.x release, so Composer's default audit policy declines to
install it at all.
Install
composer require mailkube/mailkube-laravel
The service provider is discovered automatically. Add the mailer to config/mail.php:
'mailers' => [ 'mailkube' => [ 'transport' => 'mailkube', ], ],
and point MAIL_MAILER at it:
MAIL_MAILER=mailkube MAILKUBE_API_KEY=mk_live_...
That is the whole setup. Everything else on this page is optional.
Sending
Nothing about your application changes. Mailables, Mail::to(...), queued mail, attachments,
Mail::fake() in tests: all of it works as it already did.
use Illuminate\Support\Facades\Mail; Mail::to('customer@example.com')->send(new OrderShipped($order));
What is mapped
The sender, recipients (including blind copies), subject, both bodies, attachments and your custom headers. Anything Laravel's mail API can express, this transport passes through.
Send-time features the SDK offers but Laravel's mail API has no slot for — tags, topics, templates, scheduling, idempotency keys — are reached by using the SDK directly:
app(\Mailkube\Client::class)->emails->send( from: 'Acme <hello@yourdomain.com>', to: 'customer@example.com', subject: 'Hello world', html: '<p>It works!</p>', tags: [new \Mailkube\Model\Tag('campaign', 'spring')], );
The client is bound in the container and already carries your configuration, so there is nothing to wire up.
Inline images degrade to ordinary attachments. The API's attachment model carries a filename, content and content type, with no content id, so a
cid:reference has nothing to resolve against. This is a capability of the platform rather than of this package.
Configuration
Reading order for every setting: the per-mailer array in config/mail.php, then
config/mailkube.php, then the SDK's own environment fallback.
| Setting | config/mail.php key |
Environment | Default |
|---|---|---|---|
| API key | api_key |
MAILKUBE_API_KEY |
required |
| Base URL | base_url |
MAILKUBE_BASE_URL |
the SDK's |
| Timeout | timeout |
MAILKUBE_TIMEOUT |
the SDK's |
| Log channel | MAILKUBE_LOG_CHANNEL |
none (silent) | |
| Webhook path | MAILKUBE_WEBHOOK_PATH |
none (no route) | |
| Webhook secret | MAILKUBE_WEBHOOK_SECRET |
required for webhooks | |
| Webhook tolerance | MAILKUBE_WEBHOOK_TOLERANCE |
the SDK's |
An unset setting is omitted rather than passed along as null, so the SDK's own resolution still applies.
Name a channel from config/logging.php and the SDK writes its request and response metadata
through Laravel's logging stack at debug level:
MAILKUBE_LOG_CHANNEL=stack
Bodies are never logged, and the Authorization and Idempotency-Key headers are redacted by the
SDK.
Publish the config file only if you want to edit it in place:
php artisan vendor:publish --tag=mailkube-config
Two accounts, two mailers
Give a mailer its own credentials and it gets its own client:
'mailers' => [ 'mailkube' => ['transport' => 'mailkube'], 'marketing' => [ 'transport' => 'mailkube', 'api_key' => env('MARKETING_API_KEY'), ], ],
Mail::mailer('marketing')->to($user)->send(new Newsletter());
Errors
A failed send raises Symfony\Component\Mailer\Exception\TransportException with the SDK's own
exception as its previous. That is Laravel's and Symfony's error type rather than this package's on
purpose: failover transports, queued-mail failure handling and Mail::assertNothingSent() all key
off it.
use Symfony\Component\Mailer\Exception\TransportException; try { Mail::to($user)->send(new OrderShipped($order)); } catch (TransportException $e) { report($e->getPrevious()); // the SDK exception, with the API's error name and request id }
Retry policy is deliberately not this package's business: the SDK documents which errors are safe to retry, and Laravel's queue is what retries them.
Webhooks
Off by default. A package should not mount an unauthenticated POST route just for being installed, so you opt in by naming a path:
MAILKUBE_WEBHOOK_PATH=webhooks/mailkube MAILKUBE_WEBHOOK_SECRET=whsec_...
Deliveries are verified against the raw request body and dispatched as one Laravel event:
use Mailkube\Laravel\Events\WebhookReceived; use Mailkube\Event\EmailBouncedEvent; use Mailkube\Event\EmailDeliveredEvent; class HandleMailkubeWebhook { public function handle(WebhookReceived $received): void { $event = $received->event; if ($event instanceof EmailDeliveredEvent) { Log::info('delivered', ['id' => $event->data->emailId]); } elseif ($event instanceof EmailBouncedEvent) { Suppression::add($event->data->bounce->recipient); } } }
One event class rather than one per webhook type, so an event this SDK release does not model still reaches your listener with its payload intact instead of being dropped.
Listeners run inside the request unless you queue them, and the endpoint answers as soon as they return. Queue anything slow, or delivery latency becomes retries.
The route defaults to the api middleware group, which has no CSRF verification. A
machine-to-machine POST cannot present a CSRF token, so if you move it into web you must add the
path to your CSRF exceptions or every delivery is rejected with a 419. Likewise, any middleware that
decodes and re-encodes the request body breaks the signature: it no longer covers the bytes that
were signed.
Point it at your own controller instead, if you prefer:
Route::post('/hooks/mail', \Mailkube\Laravel\Http\WebhookController::class) ->middleware('api');
Extending this package
Before adding a setting, a mapped field, or another entry point, read
.rules/INTEGRATION_CONTRACT.md (what every mailkube framework
integration does identically) and .rules/LARAVEL_INTEGRATION.md
(how those rules land here). Both carry a checklist.
The short version: the capability has to exist in the SDK first, inputs are mapped in
src/Payload.php and never at a call site, and configuration goes through src/Config.php.
This package ships no examples/ directory, deliberately: every entry point it exposes needs a host
application before it can run. This README is the wiring documentation, and the test suite is what
keeps it honest.
Contributing
See CONTRIBUTING.md for the development setup and the quality gates every change must pass. Security issues: see SECURITY.md.
License
Apache-2.0 © 2026 Mailtactic, Corp..