├── .editorconfig ├── .github └── workflows │ └── tests.yaml ├── .gitignore ├── LICENSE.md ├── composer.json ├── phpunit.xml ├── readme.md ├── src ├── MandrillServiceProvider.php ├── MandrillTransport.php └── MandrillTransportException.php └── tests └── MandrillTransportTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.yaml] 2 | indent_size = 2 3 | indent_style = space 4 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 'on': 3 | push: null 4 | jobs: 5 | tests: 6 | runs-on: ubuntu-24.04 7 | timeout-minutes: 5 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | php: 12 | - 8.2 13 | - 8.3 14 | - 8.4 15 | laravel: 16 | - 11.* 17 | - 12.*-dev 18 | include: 19 | - laravel: 11.* 20 | testbench: 9.* 21 | - laravel: 12.*-dev 22 | testbench: 10.*-dev 23 | exclude: 24 | - laravel: 12.*-dev 25 | php: 8.1 26 | 27 | name: 'Tests - PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }}' 28 | steps: 29 | - name: Checkout code 30 | uses: actions/checkout@v4 31 | 32 | - name: Cache dependencies 33 | uses: actions/cache@v4 34 | with: 35 | path: ~/.composer/cache/files 36 | key: 37 | dependencies-pw-v2-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 38 | 39 | - name: Setup PHP 40 | uses: shivammathur/setup-php@v2 41 | with: 42 | php-version: '${{ matrix.php }}' 43 | extensions: 'curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv' 44 | coverage: none 45 | tools: 'composer:v2' 46 | 47 | - name: Install dependencies 48 | run: | 49 | composer --version 50 | composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update 51 | composer require "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update --dev 52 | composer update --prefer-dist --no-interaction --no-suggest --dev 53 | composer dump 54 | 55 | - name: Execute tests 56 | run: vendor/bin/phpunit 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | Thumbs.db 6 | .idea 7 | .phpunit.result.cache -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Rob Fonseca 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "therobfonz/laravel-mandrill-driver", 3 | "description": "Mandrill Driver for Laravel", 4 | "type": "library", 5 | "keywords": [ 6 | "Laravel", 7 | "Mandrill" 8 | ], 9 | "license": "MIT", 10 | "minimum-stability": "dev", 11 | "authors": [ 12 | { 13 | "name": "Luis Dalmolin", 14 | "email": "luis.nh@gmail.com" 15 | }, 16 | { 17 | "name": "Rob Fonseca", 18 | "email": "robfonseca@gmail.com" 19 | }, 20 | { 21 | "name": "Brandon Ferens", 22 | "email": "brandon@kirschbaumdevelopment.com" 23 | } 24 | ], 25 | "require": { 26 | "php": "^8.2", 27 | "illuminate/support": "^11.0|^12.0", 28 | "mailchimp/transactional": "^1.0", 29 | "symfony/mailer": "^7.0" 30 | }, 31 | "require-dev": { 32 | "mockery/mockery": "^1.4.4", 33 | "phpunit/phpunit": "^10.0|^11.0", 34 | "orchestra/testbench": "^9.9|^10.0" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "LaravelMandrill\\": "src/" 39 | } 40 | }, 41 | "extra": { 42 | "laravel": { 43 | "providers": [ 44 | "LaravelMandrill\\MandrillServiceProvider" 45 | ] 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Laravel Mandrill Driver 2 | 3 | This package re-enables Mandrill driver functionality using the Mail facade in Laravel 6+. 4 | 5 | To install the package in your project, you need to require the package via Composer: 6 | 7 | ```bash 8 | composer require therobfonz/laravel-mandrill-driver 9 | ``` 10 | 11 | To add your Mandrill secret key, add the following lines to `config\services.php` and set `MANDRILL_KEY` in your env: 12 | 13 | ```php 14 | 'mandrill' => [ 15 | 'secret' => env('MANDRILL_KEY'), 16 | ], 17 | ``` 18 | 19 | You can also add custom Mandrill headers to each email sent, for this you need to add the headers array in the following format to `config\services.php`: 20 | 21 | ```php 22 | 'mandrill' => [ 23 | 'secret' => env('MANDRILL_KEY'), 24 | 'headers' => [ 25 | 'header-example-x' => env('MANDRILL_HEADER_X'), 26 | 'header-example-y' => env('MANDRILL_HEADER_Y'), 27 | ] 28 | ], 29 | ``` 30 | all the valid options in Mandrill docs at: https://mailchimp.com/developer/transactional/docs/smtp-integration/#customize-messages-with-smtp-headers 31 | 32 | 33 | #### Accessing Mandrill message ID 34 | Mandrill message ID's for sent emails can be accessed by listening to the `MessageSent` event. It can then be read either from the sent data or the X-Message-ID header. 35 | 36 | ```php 37 | 38 | Event::listen(\Illuminate\Mail\Events\MessageSent::class, function($event) 39 | { 40 | $messageId = $event->sent->getMessageId(); 41 | $messageId = $event->message->getHeaders()->get('X-Message-ID'); 42 | } 43 | 44 | ``` 45 | 46 | ## Versions 47 | 48 | | Laravel Version | Mandrill package version | 49 | |------------------|----------------------------------| 50 | | 11|12 | 6.x | 51 | | 10 | 5.x | 52 | | 9 | 4.x | 53 | | 6, 7, 8 | 3.x | 54 | 55 | Add the Mandrill mailer to your `config\mail.php`: 56 | 57 | ```php 58 | 'mandrill' => [ 59 | 'transport' => 'mandrill', 60 | ], 61 | ``` 62 | 63 | Set the `MAIL_MAILER` value in your env to `mandrill` to enable it: 64 | 65 | ```php 66 | MAIL_MAILER=mandrill 67 | ``` 68 | 69 | ## Laravel 6 Installation 70 | 71 | As before, you can set the `MAIL_DRIVER` value in your env to `mandrill` to enable it: 72 | 73 | ```php 74 | MAIL_DRIVER=mandrill 75 | ``` 76 | 77 | ## Lumen Installation 78 | 79 | Add the following line to `bootstrap/app.php` 80 | 81 | ```php 82 | $app->register(LaravelMandrill\MandrillServiceProvider::class); 83 | ``` 84 | -------------------------------------------------------------------------------- /src/MandrillServiceProvider.php: -------------------------------------------------------------------------------- 1 | setApiKey(Config::get('services.mandrill.secret')); 22 | 23 | $headers = Config::get('services.mandrill.headers', []); 24 | 25 | return new MandrillTransport($client, $headers); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/MandrillTransport.php: -------------------------------------------------------------------------------- 1 | setHeaders($message); 35 | 36 | return parent::send($message, $envelope); 37 | } 38 | 39 | /** 40 | * {@inheritDoc} 41 | */ 42 | protected function doSend(SentMessage $message): void 43 | { 44 | $data = $this->mailchimp->messages->sendRaw([ 45 | 'raw_message' => $message->toString(), 46 | 'async' => true, 47 | 'to' => $this->getTo($message), 48 | ]); 49 | 50 | if ($data instanceof \GuzzleHttp\Exception\RequestException) 51 | { 52 | throw new MandrillTransportException($data->getMessage(), $data->getCode(), $data); 53 | } 54 | 55 | // If Mandrill _id was returned, set it as the message id for 56 | // use elsewhere in the application. 57 | if (!empty($data[0]?->_id)) { 58 | $messageId = $data[0]->_id; 59 | $message->setMessageId($messageId); 60 | // Convention seems to be to set this header on the original for access later. 61 | $message->getOriginalMessage()->getHeaders()->addHeader('X-Message-ID', $messageId); 62 | } 63 | } 64 | 65 | /** 66 | * Retrieves recipients from the original message or envelope. 67 | * 68 | * @param SentMessage $message 69 | * @return array 70 | */ 71 | protected function getTo(SentMessage $message): array 72 | { 73 | $recipients = []; 74 | 75 | $original_message = $message->getOriginalMessage(); 76 | 77 | if ($original_message instanceof Email) { 78 | 79 | if (!empty($original_message->getTo())) { 80 | foreach ($original_message->getTo() as $to) { 81 | $recipients[] = $to->getEncodedAddress(); 82 | } 83 | } 84 | 85 | if (!empty($original_message->getCc())) { 86 | foreach ($original_message->getCc() as $cc) { 87 | $recipients[] = $cc->getEncodedAddress(); 88 | } 89 | } 90 | 91 | if (!empty($original_message->getBcc())) { 92 | foreach ($original_message->getBcc() as $bcc) { 93 | $recipients[] = $bcc->getEncodedAddress(); 94 | } 95 | } 96 | } 97 | 98 | // Fall-back to envelope recipients 99 | if (empty($recipients)) { 100 | foreach ($message->getEnvelope()->getRecipients() as $recipient) { 101 | $recipients[] = $recipient->getEncodedAddress(); 102 | } 103 | } 104 | 105 | return $recipients; 106 | } 107 | 108 | /** 109 | * Set headers of email. 110 | * 111 | * @param Message $message 112 | * 113 | * @return Message 114 | */ 115 | protected function setHeaders(Message $message): Message 116 | { 117 | $messageHeaders = $message->getHeaders(); 118 | $messageHeaders->addTextHeader('X-Dump', 'dumpy'); 119 | 120 | foreach ($this->headers as $name => $value) { 121 | $messageHeaders->addTextHeader($name, $value); 122 | } 123 | 124 | return $message; 125 | } 126 | 127 | /** 128 | * Get the string representation of the transport. 129 | * 130 | * @return string 131 | */ 132 | public function __toString(): string 133 | { 134 | return 'mandrill'; 135 | } 136 | 137 | /** 138 | * Replace Mandrill client. 139 | * This is used primarily for testing but could in theory allow other use cases 140 | * e.g. Configuring proxying in Guzzle. 141 | * 142 | * @param ApiClient $client [description] 143 | * @return void 144 | */ 145 | public function setClient(ApiClient $client): void 146 | { 147 | $this->mailchimp = $client; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/MandrillTransportException.php: -------------------------------------------------------------------------------- 1 | set('mail.driver', 'mandrill'); 30 | $app['config']->set('services.mandrill.headers', [ 31 | 'X-MC-Subaccount' => 'hello_world' 32 | ]); 33 | } 34 | 35 | /** 36 | * Check Mandrill message id is returned. 37 | * 38 | */ 39 | public function testMessageIdIsReturned() 40 | { 41 | // tracks activity in the Mock 42 | $history = []; 43 | 44 | // Mock Mandrill API as being successful. 45 | $mock = new MockHandler([ 46 | new Response(200, 47 | ['content-type' => 'application/json'], 48 | json_encode([ 49 | [ 50 | "email" => "testemail@example.com", 51 | "status" => "queued", 52 | "_id" => "111111111111111" 53 | ] 54 | ]) 55 | ) 56 | ]); 57 | $this->mockMandrillAPiResponces($mock, $history); 58 | 59 | // Setup test email. 60 | $testMail = $this->getMailable(); 61 | 62 | // Ensure event contains expected data. 63 | Event::listen(MessageSent::class, function($event) 64 | { 65 | // Check Mandrill _id was passed back 66 | $this->assertEquals($event->sent->getMessageId(), "111111111111111"); 67 | $this->assertEquals($event->message->getHeaders()->get('X-Message-ID')->getValue(), "111111111111111"); 68 | 69 | // Check correct from email. 70 | $this->assertEquals($event->message->getFrom()[0]->getAddress(), "mandrill@test.com"); 71 | // Check correct to email. 72 | $this->assertEquals($event->message->getTo()[0]->getAddress(), "testemail@example.com"); 73 | }); 74 | 75 | // Trigger event 76 | Mail::to('testemail@example.com')->send($testMail); 77 | 78 | // Ensure data all got posted to expected locations 79 | $this->assertEquals($history[0]['request']->getMethod(), 'POST'); 80 | $this->assertEquals($history[0]['request']->getRequestTarget(), '/api/1.0/messages/send-raw'); 81 | $this->assertCount(1, $history); 82 | } 83 | 84 | public function testHeadersAreSent() 85 | { 86 | // tracks activity in the Mock 87 | $history = []; 88 | 89 | // Mock Mandrill API as being successful. 90 | $mock = new MockHandler([ 91 | new Response(200, 92 | ['content-type' => 'application/json'], 93 | json_encode([ 94 | [ 95 | "email" => "testemail@example.com", 96 | "status" => "queued", 97 | "_id" => "111111111111111" 98 | ] 99 | ]) 100 | ) 101 | ]); 102 | $this->mockMandrillAPiResponces($mock, $history); 103 | 104 | $testMail = $this->getMailable(); 105 | 106 | // Trigger event 107 | Mail::to('testemail@example.com')->send($testMail); 108 | 109 | $payload = urldecode($history[0]['request']->getBody()->getContents()); 110 | 111 | // Ensure headers are set. 112 | $this->assertStringContainsString("X-MC-Subaccount: hello_world", $payload); 113 | $this->assertStringContainsString("X-Dump: dumpy", $payload); 114 | $this->assertStringContainsString("Subject: Testing things", $payload); 115 | } 116 | 117 | public function testThrowsException() 118 | { 119 | // tracks activity in the Mock 120 | $history = []; 121 | 122 | // Mock Mandrill API as being successful. 123 | $mock = new MockHandler([ 124 | new Response(500, 125 | ['content-type' => 'application/json'], 126 | json_encode([ 127 | [ 128 | "type" => "https://mailchimp.com/developer/marketing/docs/errors/", 129 | "title" => "Internal Server Error", 130 | "status" => 500, 131 | "detail" => "Internal Server Error", 132 | ] 133 | ]) 134 | ) 135 | ]); 136 | $this->mockMandrillAPiResponces($mock, $history); 137 | 138 | $testMail = $this->getMailable(); 139 | 140 | $this->expectException(MandrillTransportException::class); 141 | $this->expectExceptionCode(500); 142 | $this->expectExceptionMessage('500 Internal Server Error'); 143 | 144 | Mail::to('testemail@example.com')->send($testMail); 145 | } 146 | 147 | /** 148 | * Mock the Mandrills underlying Guzzle instance 149 | * 150 | * @param MockHandler $handler Used to define a stack of requests to mock 151 | * @param array &$container Used to view history of requests made via the mock 152 | * @return void 153 | */ 154 | protected function mockMandrillAPiResponces(MockHandler $handler, &$container): void 155 | { 156 | // Setup mocks 157 | $stackHandler = HandlerStack::create($handler); 158 | 159 | // Add history tracking middleware 160 | $history = Middleware::history($container); 161 | $stackHandler->push($history); 162 | 163 | // Inject a mocked instance of Guzzle into the underlying Mandrill transport. 164 | // This will allow us to test the mail right through to Mandrills APIs. 165 | $mockApiClient = new class($stackHandler) extends ApiClient { 166 | public function __construct($stackHandler) 167 | { 168 | parent::__construct(); 169 | // Swap in mocked Guzzle instance 170 | $this->requestClient = new Client([ 171 | 'handler' => HandlerStack::create($stackHandler) 172 | ]); 173 | } 174 | }; 175 | 176 | // Inject this into the transport within the Mail Facade 177 | Mail::getFacadeRoot()->mailer('mandrill')->getSymfonyTransport()->setClient($mockApiClient); 178 | } 179 | 180 | protected function getMailable(): Mailable 181 | { 182 | return new class() extends Mailable { 183 | public function build() 184 | { 185 | return $this->from('mandrill@test.com', 'Test') 186 | ->html('Hello World') 187 | ->subject('Testing things'); 188 | } 189 | }; 190 | } 191 | } --------------------------------------------------------------------------------