Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Mailjet Bundle Laravel Package

royalpunk/mailjet-bundle

A PHP bundle that integrates Mailjet email delivery into your application. Configure API credentials and send transactional or marketing messages through Mailjet from a simple, reusable service layer.

View on GitHub
Deep Wiki
Context7

Getting Started

This package ships under a -bundle name, which is Symfony convention, not Laravel. Before you plan any work around it, spend five minutes confirming what you actually received:

composer require royalpunk/mailjet-bundle
ls -R vendor/royalpunk/mailjet-bundle/src vendor/royalpunk/mailjet-bundle/config 2>/dev/null
cat vendor/royalpunk/mailjet-bundle/composer.json | jq '.type, .extra, .require'
  • Laravel-style: you see src/MailjetServiceProvider.php, config/mailjet.php, and extra.laravel.providers in composer.json. Auto-discovery will register it and php artisan vendor:publish --tag=mailjet should work.
  • Symfony-style: you see DependencyInjection/MailjetExtension.php, MailjetBundle.php, and type: symfony-bundle. This will not register in Laravel; see Gotchas before you invest further.

Configuration to look for first — read src/ for the exact env keys (they vary between packages):

# One of these naming schemes is likely; grep the source to confirm
MAILJET_API_KEY=
MAILJET_SECRET_KEY=
# Mailjet SDK-style alternative:
MJ_APIKEY_PUBLIC=
MJ_APIKEY_PRIVATE=

If a publishable config exists:

php artisan vendor:publish --tag=mailjet

You should get config/mailjet.php with at least public/private key entries and a sandbox flag.

Where to look first, in order:

  1. composer.json — the require block tells you the pinned Mailjet SDK major and the illuminate/* ranges.
  2. The service provider's register() — this reveals the bound singleton/class name (e.g. Royalpunk\MailjetBundle\Client) and how the API keys are read.
  3. Any facade under src/Facades/ — if one exists, that's your day-to-day entry point.

First use case — send a transactional email (class name is illustrative; adjust to what register() binds):

use Royalpunk\MailjetBundle\Client;

class SendWelcomeEmail
{
    public function __construct(private Client $mailjet) {}

    public function handle(string $to, string $name): void
    {
        $this->mailjet->post(\Mailjet\Resources::$Email, [
            'body' => [
                'FromEmail' => ['Email' => '[email protected]', 'Name' => config('app.name')],
                'To'        => [['Email' => $to, 'Name' => $name]],
                'Subject'   => 'Welcome aboard',
                'TextPart'  => 'Hello!',
                'HTMLPart'  => '<p>Hello!</p>',
            ],
        ]);
    }
}

Send one message against a Mailjet sandbox address before building anything else. Confirm the response object has an HTTP status you can inspect — the whole design of your error handling depends on whether failures throw or return a boolean.

Implementation Patterns

Wrap it immediately behind a project-owned interface. This is the single most important pattern for a thin wrapper whose upstream is unmaintained. It is also your migration escape hatch if the forge disappears.

// app/Support/Mail/Transport.php
interface Transport
{
    public function send(OutboundMessage $message): void;
}

// app/Support/Mail/MailjetTransport.php
final class MailjetTransport implements Transport
{
    public function __construct(private Client $client) {}

    public function send(OutboundMessage $message): void
    {
        $response = $this->client->post(\Mailjet\Resources::$Email, [
            'body' => $message->toMailjetPayload(),
        ]);

        if ((int) $response->getStatus() >= 400) {
            throw new MailDeliveryFailed($response->getReasonPhrase(), $response->getStatus());
        }
    }
}

Bind it in a project-level provider: $this->app->bind(Transport::class, MailjetTransport::class);

Always queue sends. Transactional mail is exactly the kind of work that must not block a request cycle. Use a dedicated job with ShouldQueue, $tries = 3, and an explicit $backoff = [10, 60, 300];. Never call the client inline from a controller.

class SendTransactionalEmail implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;
    public array $backoff = [10, 60, 300];

    public function __construct(public OutboundMessage $message) {}

    public function handle(Transport $transport): void
    {
        $transport->send($this->message);
    }

    public function retryUntil(): DateTimeInterface
    {
        return now()->addHours(6);
    }
}

Retry semantics live in the job, not the wrapper. The bundle almost certainly does not classify errors for you.

Template sends. If the package proxies the Mailjet API you can pass Mailjet-hosted template IDs and variables; if it only offers a send(subject, body)-style method you are limited to raw MIME. Verify before promising marketing that templates are supported:

'TemplateID'           => 1234567,
'TemplateLanguage'     => true,
'TemplateErrorReporting' => ['Email' => '[email protected]'],
'Variables'            => ['first_name' => 'Ada', 'plan' => 'pro'],

If TemplateID isn't surfaced by the wrapper, drop to the underlying mailjet/mailjet-apiv3-php client directly rather than fighting the abstraction.

Integration in a Laravel app with Mailables. Do not build a custom transport around this bundle to satisfy Mail::send(). That path already exists in the framework (MAIL_MAILER=mailjet plus a DSN via symfony/mailjet-mailer). Use this bundle for explicit, hand-shaped sends — onboarding drips, receipt emails with specific Mailjet headers, per-tenant From addresses — where you want to control the payload directly.

Testing. The bundle won't provide fakes. Build your own seam: bind a NullTransport or RecordingTransport in tests/, assert on OutboundMessage objects, and keep one integration test that hits a Mailjet sandbox on a schedule — not on every CI run.

$this->app->instance(Transport::class, $recorder = new RecordingTransport());
// ... trigger the flow ...
$this->assertCount(1, $recorder->sent);
$this->assertSame('welcome', $recorder->sent[0]->template);

Adopt as a fork, not a remote. Because the repo lives on a self-hosted forge, add a vcs entry only as a bootstrap step, then copy the source into app/Support/Mailjet (or an internal package) and pin a commit. Record the SHA and the upstream URL in a README so a future maintainer can diff. Consuming it live means every CI runner needs forge credentials, and every build breaks if the forge is down.

Gotchas and Tips

It is very likely a Symfony bundle. Laravel's package manifest will not pick it up, config/mailjet.php won't be published, and no service will be bound. Options, in order of sanity: (a) don't use it — use Laravel's built-in Mailjet transport or the official SDK bound in your own provider; (b) vendor the single client class and register it yourself; (c) full Symfony Bundle adapter — not worth it for a one-email wrapper.

Composer cannot resolve a forge-only package from Packagist. You will need a vcs repository entry and credentials on every developer machine and CI runner. That is a build-time fragility unrelated to your code — another argument for vendoring.

Silent failures are the real risk. Thin wrappers often return false or swallow exceptions from post(). Password resets that "work" but never arrive are the worst outcome in this category. Always inspect getStatus() on the response and throw on non-2xx. Add a Log::error with the Mailjet status and message ID on failure — never log the payload, it contains recipient PII and possibly credentials if the From/To config is malformed.

Distinguish retryable from terminal. Mailjet returns 429 for rate limits and 5xx for transient faults — safe to retry. 400/422 mean a malformed payload or invalid sender — retrying just produces a queue storm. Classify in the Transport implementation:

throw match (true) {
    $status === 429 || $status >= 500 => new RetryableDeliveryFailed($reason, $status),
    default                            => new MailDeliveryFailed($reason, $status),
};

Then let the job fail permanently on the terminal case.

Config caching bites. If the provider reads env('MAILJET_API_KEY') directly rather than through config(), php artisan config:cache leaves you with null keys in production. Verify the provider uses config('mailjet.api_key') and that config/mailjet.php references env(...) — those are the only two places env() is allowed.

The pinned SDK major is your upgrade risk. composer.json will pin mailjet/mailjet-apiv3-php to a major. If that major is old (or the illuminate/* range is ^8|^9 while you're on 11), you face a fork-and-bump before you can upgrade Laravel. Check this before adoption, not after.

Attachments are a memory risk under load. If the wrapper supports them, stream from a path or pre-generate signed URLs rather than base64-inlining large PDFs inside queued jobs. A queue worker with 20 concurrent receipt emails holding 5 MB attachments each will OOM.

No webhook or event handling. This bundle handles outbound send. Bounce/complaint webhooks, suppression list management, and per-tenant sending domains are not provided — you will build them yourself against the Mailjet API directly. Do not let a product spec assume otherwise.

Extension point worth knowing. The bundle almost certainly exposes the underlying Mailjet\Client (whether via a facade or getter). That is your escape hatch for anything the wrapper doesn't cover — templates, batch sends, contact sync — without abandoning the wrapper entirely:

// Bypass the wrapper for a feature it doesn't expose
$this->client->post(\Mailjet\Resources::$Contact, [...]);

Document any such bypass so the abstraction boundary is honest.

Rotation runbook. Because there is no vendor support, write it day one: where the fork lives, which env keys hold credentials, how to rotate them in Mailjet's dashboard, and how to verify after rotation with a single sandbox send. Assume the upstream maintainer is unreachable; the runbook is the support plan.

Baseline recommendation. If the requirement is simply "send transactional mail through Mailjet from Laravel," use the framework's built-in Mailjet transport (symfony/mailjet-mailer under the hood via the MAIL_MAILER=mailjet DSN) and skip this package entirely. Keep royalpunk/mailjet-bundle as reference material for the payload shape if you need custom fields, and route those specific cases through your own Transport implementation against the official SDK.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log