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

Mandrill Bundle Laravel Package

hometogo/mandrill-bundle

Symfony bundle for integrating Mandrill transactional email, with simple configuration and a SwiftMailer transport for sending messages and templates through Mandrill’s API.

View on GitHub
Deep Wiki
Context7

Getting Started

Do not composer require this package. It is a Symfony2 bundle (hometogo/mandrill-bundle), not a Laravel package. Its composer.json will pull symfony/framework-bundle ~2.x, swiftmailer/swiftmailer, and PHP 5.x/7.x-era constraint sets that hard-conflict with Laravel 10/11/12 on PHP 8.2+. The realistic "getting started" is: clone the repo into a scratch directory, read three files, and port the one that matters.

Read these first, in this order:

  1. The transport class — Transport/MandrillTransport.php (or similarly named). This is the only artifact worth keeping. It implements Swift_Transport (isStarted(), start(), stop(), ping(), send()), builds the Mandrill payload (recipients, merge_vars, tags, attachments, headers, async), POSTs to https://mandrillapp.com/api/1.0/messages/send.json, and maps the response array. Everything you need to know about the API shape is in send().
  2. The DI extension (DependencyInjection/*Extension.php) — this is the config schema: API key, async, subaccount, logging. It tells you which per-message options the author considered first-class.
  3. The compiler pass / service definition — how the transport gets registered into Symfony's mailer. You will replace this with Mail::extend().

Skip the bundle class, the Resources/config/*.xml, and the tests (there likely aren't any). Roughly 80% of the package is Symfony wiring you cannot use.

First use case — the one that earns the port: a password-reset mail, sent through Laravel's Mail facade, that lands in the provider with a tag identifying the notification type. Concretely:

// app/Mail/PasswordReset.php
class PasswordReset extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(public User $user, public string $token) {}

    public function build()
    {
        return $this->subject('Reset your password')
            ->tag('password-reset')          // -> Mandrill `tags`
            ->with(['reset_url' => route('password.reset', $this->token)]);
    }

    public function headers(): Headers
    {
        return (new Headers)->addTextHeader('X-MC-Metadata', json_encode(['user_id' => $this->user->id]));
    }
}

That mail should work identically whether you land it over SMTP (Pattern A below) or over a custom transport (Pattern B). Get the mailables written against the facade before you decide you need a transport at all — most teams don't.

Decide this before writing code: do you hold a legacy Mandrill account, or would you be integrating with Mailchimp Transactional (Mandrill was folded into Mailchimp in 2016)? If the latter, the current SDK is mailchimp/mailchimp-transactional, and this bundle's raw REST calls are a stale reference. Verify in the Mailchimp dashboard before anything else.

Implementation Patterns

Three patterns, in ascending order of effort. Ship pattern A; only escalate if you can name the feature SMTP cannot express.

Pattern A — SMTP through Laravel's stock transport (recommended default).

// config/mail.php
'mailers' => [
    'mandrill' => [
        'transport' => 'smtp',
        'host' => env('MANDRILL_SMTP_HOST', 'smtp.mandrillapp.com'),
        'port' => 587,
        'encryption' => 'tls',
        'username' => env('MANDRILL_SMTP_USERNAME'),
        'password' => env('MANDRILL_API_KEY'),
    ],
],

Zero custom code, zero forked files, reversible by changing MAIL_MAILER. This covers password resets, receipts, invites, and notifications — everything fire-and-forget. Confirm the current SMTP host and username convention in the Mailchimp Transactional dashboard; the credentials shape has changed over the years and blog posts from the Mandrill era are wrong.

Pattern B — custom transport backed by the official SDK or Http.

Only when you need per-message options SMTP cannot express: tags, merge_vars beyond header-based ones, subaccount, async, track_opens/track_clicks, or metadata. Implement Laravel's contract and register it:

// app/Mail/Transport/MandrillTransport.php
namespace App\Mail\Transport;

use Illuminate\Mail\Transport\Transport;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\Address;
use Illuminate\Support\Facades\Http;

class MandrillTransport extends Transport
{
    public function __construct(
        private string $key,
        private bool $async = false,
        private ?string $subaccount = null,
    ) {}

    public function send(Email $message, ?Envelope $envelope = null): void
    {
        $payload = [
            'key' => $this->key,
            'message' => [
                'html' => $message->getHtmlBody(),
                'text' => $message->getTextBody(),
                'subject' => $message->getSubject(),
                'from_email' => $message->getFrom()[0]->getAddress(),
                'from_name' => $message->getFrom()[0]->getName(),
                'to' => array_map(fn (Address $a) => ['email' => $a->getAddress(), 'name' => $a->getName(), 'type' => 'to'], $message->getTo()),
                'headers' => $this->headers($message),
                'tags' => $this->tags($message),
                'attachments' => $this->attachments($message),
                'metadata' => $this->metadata($message),
            ],
            'async' => $this->async,
        ];

        if ($this->subaccount) {
            $payload['message']['subaccount'] = $this->subaccount;
        }

        $response = Http::timeout(10)->acceptJson()->post('https://mandrillapp.com/api/1.0/messages/send.json', $payload);

        // Per-recipient status inspection is mandatory — see Gotchas.
        $this->guardResponse($response->json(), $message);
    }
}

Register it without touching the framework:

// app/Providers/MailServiceProvider.php
public function boot(): void
{
    Mail::extend('mandrill', fn () => new Mailer(
        app('view'),
        new MandrillTransport(
            config('services.mandrill.key'),
            async: (bool) config('services.mandrill.async'),
            subaccount: config('services.mandrill.subaccount'),
        ),
        app('events'),
    ));
}
// config/mail.php
'mailers' => [
    'mandrill' => ['transport' => 'mandrill'],
],

Pattern C — adopting the bundle itself. Not defensible in a Laravel estate. The bundle-loading mechanism, DI extension, and compiler pass do not exist in Laravel; you would end up discarding everything except the transport — which is Pattern B. If you truly need the bundle, you are on Symfony2, which is a platform decision, not a mail decision.

Keep call sites on the facade. Mailers must depend on Mail, Notification, or a domain-level Mailer interface — never a concrete transport. This is what makes A→B a config change rather than a refactor, and it is what lets you swap providers entirely when Mandrill's pricing changes again.

Queue by default. Wrap every non-latency-critical mail in ShouldQueue, give it $tries = 3, $backoff = [10, 60, 300], and a retryUntil. Password resets should stay synchronous or queued on a fast worker; receipts and invites should queue. This is the retry layer the bundle never had, and it is where the operational resilience lives.

Test the transport, fake the facade. Feature tests use Mail::fake() and assertQueued/assertSent. Transport unit tests use Http::fake() and assert on the recorded request body — that's how you pin the payload shape (tags, merge vars, per-recipient type) and catch provider-API drift in CI rather than in a 401 at 2am.

Config placement. API key in config/services.php (services.mandrill.*), read via env() only inside config files. Non-secret knobs (async, subaccount, tags-per-mailable) can be config too, but tag names belong next to the mailable that emits them so they stay greppable.

Gotchas and Tips

It will not install, and "it installed" is a red flag. Any resolution that succeeds against a modern Laravel is either a stale lockfile or a transitive dependency you have not audited. Do not force-resolve.

Swiftmailer is abandoned (Nov 2021) and Laravel no longer uses it. Laravel 9+ uses symfony/mailer and Symfony\Component\Mime\Email. Every Swiftmailer-era API in the bundle — Swift_Message, Swift_Attachment, Swift_Transport — has no counterpart in your runtime. You are translating, not integrating.

Mandrill returns HTTP 200 for rejected mail. This is the single most important gotcha. messages/send.json responds with an array, one entry per recipient, each with "status": "sent" | "queued" | "rejected" | "invalid". A hard bounce or a suppressed address looks like success at the HTTP layer. Your transport must iterate the array and throw Symfony\Component\Mailer\Exception\TransportException (or your own) on any non-sent/queued status, otherwise your queue marks jobs done and mail silently disappears. The bundle does not do this robustly — check the response-parsing code and fix it in the port.

No idempotency keys. The send API has no dedupe token. Retries after a timeout can double-send. Mitigate application-side: unique jobs (ShouldBeUnique), a sent_at column on the notification record, and metadata carrying your message ID so webhooks can be reconciled.

Timeout defaults matter. Call the API with an explicit connect timeout (2–5s) and total timeout (~10s). A hung Guzzle request inside a worker is a stuck queue, and mail is on the critical path of password reset.

Webhooks are yours to build. The bundle does nothing here — no bounce, complaint, or open/click ingestion. You need an endpoint that verifies the signature (X-Mandrill-Signature — HMAC-SHA1 of the URL with the webhook key, base64), then processes hard_bounce, soft_bounce, spam, unsub, and reject events into a suppression list. Without this, you accumulate suppression debt invisibly and IP reputation decays with no alert.

async: true is not a delivery guarantee. It queues on Mandrill's side and returns immediately. Fine for newsletters, wrong for password resets where users are staring at a spinner. Route by mailable class, not globally.

Set text() on every mailable. HTML-only messages score worse with spam filters and the bundle-era examples often omitted the plaintext part. Symfony Mime makes this a one-liner; do it.

DKIM/SPF/DMARC is outside the package entirely. Configure the sending domain in Mailchimp Transactional, add the DKIM keys to DNS, and publish a DMARC policy before you send a single production message. No library solves deliverability.

Config caching bites. env() called anywhere outside config/*.php returns null after php artisan config:cache. Put the API key in config/services.php and read it via config(), or you get an opaque 500 in production only.

Extension point, if you must fork: keep the change surface to (a) response-array validation, (b) PSR-3 logging of the raw response including the _id, and (c) a payload-mapping method separated from the HTTP call so you can unit-test it without a fake. That's roughly 150–250 lines total — cheaper to own and review than any dependency, and it gives you a written owner and a deletion path when you change providers.

Log the provider's _id. It is the only thread that joins an application log line to a delivery event in Mailchimp. Without it, "did the receipt send?" is unanswerable.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle