knplabs/knp-mailjet-bundle
Symfony bundle that integrates the Mailjet email delivery service, giving you simple configuration and service access for sending transactional emails via Mailjet's API inside your Symfony application.
First, the honest framing: knplabs/knp-mailjet-bundle is a Symfony2 bundle — archived since 2016, last tagged release July 2016, 10 stars, zero dependents. You cannot composer require it into a Laravel app and get anything usable: it registers via KnpMailjetBundle::class in a Symfony kernel, ships a DependencyInjection extension, and wires a Swiftmailer transport (Swiftmailer itself is EOL). Treat the repo as reference material, not a dependency.
What to read first (30 minutes, tops):
DependencyInjection/Configuration.php and KnpMailjetExtension.php — this is where api_key, secret_key, and the sandbox call flag are defined. That config shape is the only thing worth copying.MailjetTransport / MailjetApiTransport) — it's ~100 lines and shows the entire "send one message through Mailjet's API and map the response" flow. That's the reusable idea.README.md — confirms the two integration modes: SMTP-ish transport vs raw API client.Minimal steps to get the equivalent working in Laravel:
composer require mailjet/mailjet-apiv3-php
// config/services.php
'mailjet' => [
'key' => env('MAILJET_APIKEY'),
'secret' => env('MAILJET_APISECRET'),
'sandbox'=> env('MAILJET_SANDBOX', false),
],
// app/Providers/AppServiceProvider.php — bind the client once
$this->app->singleton(\Mailjet\Client::class, fn () => new \Mailjet\Client(
config('services.mailjet.key'),
config('services.mailjet.secret'),
true,
['version' => 'v3.1']
));
First use case: a single transactional send — password reset or order confirmation — through API v3.1, verifying in the Mailjet dashboard that the message lands. Do this before touching templates or webhooks; it validates credentials and the sandbox flag in one shot.
If you'd rather not hand-roll it, the official mailjet/laravel-mailjet wrapper is the pragmatic choice, and newer Laravel versions may ship a first-party Mailjet mailer — check config/mail.php in your installed version before adding any package at all.
Pattern 1 — Thin client binding (most common). Bind Mailjet\Client as a singleton (above) and inject it into a small MailjetMailer service. Keeps the vendor SDK out of controllers and gives you one place to swap in a fake for tests. This is the direct translation of what the bundle's DI extension did.
Pattern 2 — Custom Laravel mail transport. If you want Mail::to(...)->send(new OrderShipped($order)) to route through Mailjet's API instead of SMTP, implement Symfony\Component\Mailer\Transport\TransportInterface (Laravel 9+) that calls $client->post(Resources::$Email, ['body' => $payload]), then register it in a service provider:
Mail::extend('mailjet-api', fn (array $config) => new MailjetTransport($client));
// then MAIL_MAILER=mailjet-api
This is the modern equivalent of the bundle's Swiftmailer transport, and it's the pattern worth stealing from the repo — the bundle's transport class is effectively a worked example of the payload shape (From, To, Subject, TextPart, HTMLPart).
Pattern 3 — Templated sends. Mailjet's real leverage is server-side templates. Store the template ID in config and pass variables, rather than building HTML in PHP:
$body = [
'From' => ['Email' => config('mail.from.address'), 'Name' => config('mail.from.name')],
'To' => [['Email' => $user->email, 'Name' => $user->name]],
'TemplateID' => config('services.mailjet.templates.order_shipped'),
'TemplateLanguage' => true,
'Variables' => ['order_ref' => $order->reference, 'tracking' => $order->tracking_url],
];
Note SandboxMode => true in the body when the bundle's call: false equivalent is set — keep that toggle in config, never in a hardcoded branch.
Pattern 4 — Laravel-native integration tips.
Http::fake() or mock the bound Client in tests — never hit the live API from a test suite.MessageID and Messages[0]->To[0]->MessageID on every send; it's the only way to reconcile delivery later.MailSent) from your wrapper so analytics/webhooks aren't bolted on ad hoc.Compatibility is a wall, not a speed bump. The bundle's composer.json targets Symfony 2.x and PHP 5.x-era constraints, and its Swiftmailer transport is built against Swift_Transport, which Laravel dropped in favor of Symfony Mailer. Attempting to install it will either fail resolution or drag in abandoned packages. Don't.
HTTP 200 does not mean delivered. Mailjet returns 200 with a body that can contain per-recipient failures. The bundle's transport checked the response status but the API's Messages[].Status is where the truth lives (success, error, queued). Always inspect it and throw on anything but success, or your failures will be silent.
Two different credential pairs. API v3 uses APIKey + APISecret. SMTP relay uses a different username/password pair from the same dashboard. The bundle predates v3.1 and its config naming is easy to conflate with SMTP credentials — keep them in distinct env vars (MAILJET_APIKEY/MAILJET_APISECRET vs MAILJET_SMTP_USER/MAILJET_SMTP_PASS) and never share.
Sandbox toggles silently swallow mail. Mailjet's SandboxMode returns a valid-looking response with a MessageID but delivers nothing. This is the #1 "why isn't mail arriving in staging" call. Gate it strictly on app()->environment() and log loudly when it's on.
Template variables are typed, not strings. A numeric order ID passed as a string renders fine in {{var:order_ref}} but breaks numeric filters and conditionals. Cast at the boundary: (int), (float), (bool). Mailjet will not coerce for you.
Config caching will bite you. If you read config('services.mailjet.*') inside a closure that's resolved from a cached container (php artisan config:cache), stale values survive deploys. Read config inside the singleton closure, not at class-construction top level, and clear cache in your deploy script.
Extension points worth building yourself, since the bundle offers none: a MailjetClientFactory you can swap for a fake in tests, a Transport decorator that adds retry-with-backoff on 429, and a webhook controller for bounce/spam events (the bundle never handled inbound events — that's a gap, not a feature).
Rate limits and batching. Mailjet's send endpoint accepts arrays of messages — batch notifications rather than looping one API call per recipient. The bundle's transport was one-message-per-send by design; don't inherit that bottleneck.
Last word on leverage: the architecture here (DI-configured API client + a transport adapter + a sandbox flag) is genuinely the right shape and translates cleanly to Laravel. The code does not. Budget an hour to read it, zero hours to install it.
How can I help you explore Laravel packages today?