3

I need to add a new transport driver to Laravel's mail package so that I can send e-mails through an external service (Mailjet) that isn't supported by default.

Writing the transport driver won't be a problem, but I can't find a way to hook in and add a new one so I can continue to use Laravel's mailer as normal. I can't find any documentation on extending the Mailer.

The only way I can come up with would be to replace where Laravel's MailServiceProvider is being referenced in config/app.php with my own service provider, which I could then use to register my own TransportManager and my own transport driver.

Is there a better way to add another transport driver?

Jonathon
  • 15,873
  • 11
  • 73
  • 92
  • 1
    Yes, you have to create your own `Mailer` class. Preferably, your class should implement [Mailer](https://github.com/laravel/framework/blob/5.1/src/Illuminate/Contracts/Mail/Mailer.php) and [MailQueue](https://github.com/laravel/framework/blob/5.1/src/Illuminate/Contracts/Mail/MailQueue.php) interfaces. – Skysplit Aug 23 '16 at 13:31
  • Thanks for your reply. That does seem a bit over the top just to be able to add my own driver for another service. I would rather hook into as much of Laravel's functionality as possible. See my answer below for how I've managed to implement it so far. – Jonathon Aug 23 '16 at 19:21

1 Answers1

12

Well I've managed to get it working in the way I suggested in my question (By writing my own ServiceProvider and TransportManager to allow me to provider a driver). This is what I've done for anyone that might come across this:

config/app.php - Replace Laravel's MailServiceProvider with my own

// ...
'providers' => [
    // ...

    // Illuminate\Mail\MailServiceProvider::class,
    App\MyMailer\MailServiceProvider::class,

    // ...

app/MyMailer/MailServiceProvider.php - Create a service provider that extends Laravel's MailServiceProvider and override the registerSwiftTransport() method

<?php

namespace App\MyMailer;

class MailServiceProvider extends \Illuminate\Mail\MailServiceProvider
{
    public function registerSwiftTransport()
    {
        $this->app['swift.transport'] = $this->app->share(function ($app) {
            // Note: This is my own implementation of transport manager as shown below
            return new TransportManager($app);
        });
    }
}

app/MyMailer/TransportManager.php - Add a createMailjetDriver method which makes my MailjetTransport driver available to Laravel's Mailer

<?php

namespace App\MyMailer;

use App\MyMailer\Transport\MailjetTransport;

class TransportManager extends \Illuminate\Mail\TransportManager
{
    protected function createMailjetDriver()
    {
        $config = $this->app['config']->get('services.mailjet', []);

        return new MailjetTransport(
            $this->getHttpClient($config),
            $config['api_key'],
            $config['secret_key']
        );
    }
}

app/MyMailer/Transport/MailjetTransport.php - My own Transport driver that sends e-mails through Mailjet.

Updated to contain my implementation of the Mailjet transport driver. Using the Mailjet guide for sending a basic e-mail through their API as a base.

<?php

namespace App\MyMailer\Transport;

use GuzzleHttp\ClientInterface;
use Illuminate\Mail\Transport\Transport;
use Swift_Mime_Message;

class MailjetTransport extends Transport
{
    /**
     * Guzzle HTTP client.
     *
     * @var ClientInterface
     */
    protected $client;

    /**
     * The Mailjet "API key" which can be found at https://app.mailjet.com/transactional
     *
     * @var string
     */
    protected $apiKey;

    /**
     * The Mailjet "Secret key" which can be found at https://app.mailjet.com/transactional
     *
     * @var string
     */
    protected $secretKey;

    /**
     * The Mailjet end point we're using to send the message.
     *
     * @var string
     */
    protected $endPoint = 'https://api.mailjet.com/v3/send';

    /**
     * Create a new Mailjet transport instance.
     *
     * @param  \GuzzleHttp\ClientInterface $client
     * @param $apiKey
     * @param $secretKey
     */
    public function __construct(ClientInterface $client, $apiKey, $secretKey)
    {
        $this->client = $client;
        $this->apiKey = $apiKey;
        $this->secretKey = $secretKey;
    }

    /**
     * Send the given Message.
     *
     * Recipient/sender data will be retrieved from the Message API.
     * The return value is the number of recipients who were accepted for delivery.
     *
     * @param Swift_Mime_Message $message
     * @param string[] $failedRecipients An array of failures by-reference
     *
     * @return int
     */
    public function send(Swift_Mime_Message $message, &$failedRecipients = null)
    {
        $this->beforeSendPerformed($message);

        $payload = [
            'header' => ['Content-Type', 'application/json'],
            'auth' => [$this->apiKey, $this->secretKey],
            'json' => []
        ];

        $this->addFrom($message, $payload);
        $this->addSubject($message, $payload);
        $this->addContent($message, $payload);
        $this->addRecipients($message, $payload);

        return $this->client->post($this->endPoint, $payload);
    }

    /**
     * Add the from email and from name (If provided) to the payload.
     *
     * @param Swift_Mime_Message $message
     * @param array $payload
     */
    protected function addFrom(Swift_Mime_Message $message, &$payload)
    {
        $from = $message->getFrom();

        $fromAddress = key($from);
        if ($fromAddress) {
            $payload['json']['FromEmail'] = $fromAddress;

            $fromName = $from[$fromAddress] ?: null;
            if ($fromName) {
                $payload['json']['FromName'] = $fromName;
            }
        }
    }

    /**
     * Add the subject of the email (If provided) to the payload.
     *
     * @param Swift_Mime_Message $message
     * @param array $payload
     */
    protected function addSubject(Swift_Mime_Message $message, &$payload)
    {
        $subject = $message->getSubject();
        if ($subject) {
            $payload['json']['Subject'] = $subject;
        }
    }

    /**
     * Add the content/body to the payload based upon the content type provided in the message object. In the unlikely
     * event that a content type isn't provided, we can guess it based on the existence of HTML tags in the body.
     *
     * @param Swift_Mime_Message $message
     * @param array $payload
     */
    protected function addContent(Swift_Mime_Message $message, &$payload)
    {
        $contentType = $message->getContentType();
        $body = $message->getBody();

        if (!in_array($contentType, ['text/html', 'text/plain'])) {
            $contentType = strip_tags($body) != $body ? 'text/html' : 'text/plain';
        }

        $payload['json'][$contentType == 'text/html' ? 'Html-part' : 'Text-part'] = $message->getBody();
    }

    /**
     * Add to, cc and bcc recipients to the payload.
     *
     * @param Swift_Mime_Message $message
     * @param array $payload
     */
    protected function addRecipients(Swift_Mime_Message $message, &$payload)
    {
        foreach (['To', 'Cc', 'Bcc'] as $field) {
            $formatted = [];
            $method = 'get' . $field;
            $contacts = (array) $message->$method();
            foreach ($contacts as $address => $display) {
                $formatted[] = $display ? $display . " <$address>" : $address;
            }

            if (count($formatted) > 0) {
                $payload['json'][$field] = implode(', ', $formatted);
            }
        }
    }
}

In my .env file I have:

MAIL_DRIVER=mailjet

..which allows me to use Laravel's mailer package (Through the Facade or dependency injection) as normal:

\Mail::send('view', [], function($message) {
    $message->to('me@domain.com');
    $message->subject('Test');
});
Jonathon
  • 15,873
  • 11
  • 73
  • 92
  • Did you got it working at the end? Using the Mailjet API maybe? `MailjetTransport::send()` – Flo Schild Aug 23 '16 at 21:30
  • I haven't finished implementing it yet but I have previously implemented the Mailjet API, so I'm pretty confident it will work when I'm done. I'll be using Guzzle to hit their `send` end point. – Jonathon Aug 23 '16 at 21:34
  • Thanks for the tips, I'll try that too. – Flo Schild Aug 23 '16 at 22:04
  • No problem. I can possibly update my answer later when I'm done. Take a look at the `MailgunTransport` or `MandrillTransport` that come with Laravel, they'll give you a good idea of what you need to do. – Jonathon Aug 23 '16 at 22:31
  • That would be great, I have already used Guzzle, but any help could be good for everyone here anyway :) – Flo Schild Aug 23 '16 at 22:33
  • @Flo-Schield-Bobby I've updated my answer to include my current implementation. Hope it helps :) – Jonathon Aug 24 '16 at 10:47
  • Excellent, thank you again! Just to be sure, did you have to do this because of authentication, or is it possible to use the SMTP relay directly? – Flo Schild Aug 24 '16 at 11:51
  • No problem. I just preferred to do it this way, I think you can do it either way. Their documentation is really good and should help you implement it another way if you prefer. The API key and secret key are your SMTP credentials too. – Jonathon Aug 24 '16 at 13:08
  • Good to know, thanks :) If I find something useful by my side, I'll let you know. I think the mailing system slightly change with Laravel 5.3 (yet unstable), with the new concept of Notifications too. – Flo Schild Aug 24 '16 at 14:59
  • Great, thanks. Yeah that was my worry too, but it looks currently like this will still work. It seems that they've just added support for the new `Mailable` objects into their existing Mail package – Jonathon Aug 24 '16 at 15:44
  • can you update answer for laravel 6 please? @Jonathon – devmrh Nov 26 '19 at 11:03
  • I am facing weird thing, i have configured mailjet as per documentation & checked by sending mail, but its not forwarding i can see mail request seems fine by its status code by 200. Not sure what's the issue ? Any thoughts ? –  Sep 05 '20 at 11:30