mailjet/mailjet-swiftmailer
mailjet/mailjet-swiftmailer adds a Mailjet transport to Swift Mailer, letting PHP apps send transactional and bulk email through Mailjet's API. Existing Swift Mailer workflows keep building messages while Mailjet handles delivery, tracking, and scaling.
This transport only makes sense on Laravel ≤ 8 / SwiftMailer. Confirm before doing anything:
composer show swiftmailer/swiftmailer # must exist; Laravel 9+ won't have it
mailjet/mailjet-apiv3-php and swiftmailer/swiftmailer):composer require mailjet/mailjet-swiftmailer
Grab an API Key / Secret from Mailjet → Account → API Key Management (not the SMTP credentials).
Add a mailer to config/mail.php (env values must live here, not in the provider, so config:cache works):
'mailers' => [
'mailjet' => [
'transport' => 'mailjet', // matches the Mail::extend key
'key' => env('MAILJET_APIKEY'),
'secret' => env('MAILJET_SECRET'),
],
],
AppServiceProvider::boot():use Mailjet\Client;
use Mailjet\MailjetSwiftMailer\SwiftMailer\MailjetTransport;
use Illuminate\Support\Facades\Mail;
Mail::extend('mailjet', function (array $config) {
$client = new Client(
$config['key'],
$config['secret'],
true,
['version' => 'v3.1'] // use v3.1, not the client default v3
);
return new MailjetTransport($client);
});
Mail::mailer('mailjet')->to($user->email)->send(new WelcomeMail($user));
// or set MAIL_MAILER=mailjet and use Mail::to(...)->send(...)
Look first at src/SwiftMailer/MailjetTransport.php — it is the whole package (~200 lines). It shows exactly how Swift To/Cc/Bcc, parts, attachments, inline images and X-MJ-* headers are converted into a Messages payload. Then skim Mailjet's "Send API v3.1" docs, since that payload is the real contract you are debugging against.
First useful use case: transactional mail (password reset, receipts) where you want HTTP/API delivery, templating, and correlation IDs instead of SMTP.
Bind the Mailjet client as a singleton so a single HTTP client is reused across sends in a request/worker:
$this->app->singleton(Client::class, fn () => new Client(
config('mail.mailers.mailjet.key'),
config('mail.mailers.mailjet.secret'),
true,
['version' => 'v3.1']
));
Mail::extend('mailjet', fn (array $config) => new MailjetTransport(app(Client::class)));
Route only certain mailables through Mailjet without changing the app default:
Mail::mailer('mailjet')->to($user)->send(new InvoiceMail($invoice));
Queue everything. The transport is synchronous; a slow/failing Mailjet call inside a request is a latency incident. Use ShouldQueue mailables or Mail::queue, with $tries/backoff and a failed() hook that logs the Mailjet response.
Inject Mailjet headers via withSwiftMessage when you need Mailjet-hosted templates, variables, or event correlation:
public function build()
{
return $this->view('emails.welcome')
->withSwiftMessage(function (\Swift_Message $message) {
$h = $message->getHeaders();
$h->addTextHeader('X-MJ-TemplateID', '123456');
$h->addTextHeader('X-MJ-TemplateLanguage', '1');
$h->addTextHeader('X-MJ-Vars', json_encode(['name' => $this->user->name]));
$h->addTextHeader('X-MJ-CustomID', 'welcome:'.$this->user->id);
$h->addTextHeader('X-MJ-EventPayload', json_encode(['user' => $this->user->id]));
});
}
CustomID is the simplest way to correlate Mailjet's Event API/webhooks back to your records.
Always set both parts so Mailjet can build a proper multipart message:
->view('emails.receipt') // HTML
->text('emails.receipt_text') // TextPart
Attachments / embedded images are handled by the transport (including Swift_EmbeddedFile → InlinedAttachments with the Content-ID you reference as cid: in HTML). Use ->attach($path) / ->embed($path) as usual, and make sure paths exist before queueing — a file deleted between dispatch and execution fails inside the queue worker.
Keep Mail::fake() for feature tests and treat the transport as infrastructure. For transport-level checks, test against Mailjet's sandbox or a real key in a dedicated test account; do not assert on Mailjet internals in unit tests.
Wrap the API call yourself when you need Mailjet-only features the transport doesn't map (bulk Messages[] batching, contacts/lists, campaigns). A small MailjetService using the same injected Client is cleaner than subclassing.
symfony/mailjet-mailer and use a DSN (mailjet+api://KEY:SECRET@default) or plain SMTP (in-v3.mailjet.com:587). Migration path first, package second.--ignore-platform-reqs blindly.Mail::extend must run before the mailer is resolved. Register it in register()/boot() of AppServiceProvider, not lazily in a controller. A missing registration surfaces as "Unsupported mail transport [mailjet]".env() inside it, config:cache will return null in production. Env only in config/*.php.send() returns the recipient count, not a message count. A 0 return with no exception usually means the API rejected the message. Inspect $client->getResponse()->getBody() / getStatus() and log it; the Mailjet error body (StatusCode, ErrorMessage) tells you if it's an unvalidated sender, bad template ID, or oversized message.From address must be a validated sender or on an authenticated domain in Mailjet, otherwise you get a 400 at API level. Validate once per environment and document it.Mail::to($oneUserAtATime) or batched via your own API wrapper; the transport does not split for you.\Exception from the Mailjet client, not a typed transport exception). Catch broadly in the job, release with backoff, and mark failed after N attempts.failover/roundrobin mailers won't rescue you on Laravel 8 with a custom Swift transport; keep Mailjet as a named mailer and implement fallback in your own send path if uptime matters.X-* headers are generally dropped. If a field is absent from the payload, verify against MailjetTransport.php before blaming Mailjet.Log::debug() around the response body in a custom transport wrapper, and use Mailjet's message log (search by MessageID / CustomID) to confirm delivery vs. API acceptance. "Accepted by API" ≠ "delivered".MailjetTransport into your app as App\Mail\Transport\MailjetTransport, pin it to the API client directly, and drop the abandoned SwiftMailer dependency as soon as you upgrade past Laravel 8. You keep exact header mapping and gain control over batching and error typing.How can I help you explore Laravel packages today?