# How to create a custom HTTP mail driver for Laravel 7+

In this post we're going to write a mail driver for Laravel to send HTTP POST requests to an email API. The example API accepts JSON requests with the following format:

{
	"to": [
		{
			"name": "User name",
			"email": "[email protected]"
		},
		...
	],
	"cc": [
		{
			"name": "User name",
			"email": "[email protected]"
		},
		...
	],
	"bcc": [
		{
			"name": "User name",
			"email": "[email protected]"
		},
		...
	],
	"message": "<body>Message</body>",
	"subject": "Test subject"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# Create a custom mail transport class

We will use Laravel's MailgunTransport (opens new window) class as a starting point.

Create a custom mail transport class at app/CustomMailDriver/CustomTransport.php:

<?php

namespace App\CustomMailDriver;

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

class CustomTransport extends Transport
{
    /**
     * Guzzle client instance.
     *
     * @var \GuzzleHttp\ClientInterface
     */
    protected $client;

    /**
     * API key.
     *
     * @var string
     */
    protected $key;

    /**
     * The API URL to which to POST emails.
     *
     * @var string
     */
    protected $url;

    /**
     * Create a new Custom transport instance.
     *
     * @param  \GuzzleHttp\ClientInterface  $client
     * @param  string|null  $url
     * @param  string  $key
     * @return void
     */
    public function __construct(ClientInterface $client,  $url, $key)
    {
        $this->key = $key;
        $this->client = $client;
        $this->url = $url;
    }

    /**
     * {@inheritdoc}
     */
    public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null)
    {
        $this->beforeSendPerformed($message);

        $payload = $this->getPayload($message);

        $this->client->request('POST', $this->url, $payload);

        $this->sendPerformed($message);

        return $this->numberOfRecipients($message);
    }

    /**
     * Get the HTTP payload for sending the message.
     *
     * @param  \Swift_Mime_SimpleMessage  $message
     * @return array
     */
    protected function getPayload(Swift_Mime_SimpleMessage $message)
    {
        // Change this to the format your API accepts
        return [
            'headers' => [
                'Authorization' => 'Bearer ' . $this->key,
                'Accept'        => 'application/json',
            ],
            'json' => [
                'to' => $this->mapContactsToNameEmail($message->getTo()),
                'cc' => $this->mapContactsToNameEmail($message->getCc()),
                'bcc' => $this->mapContactsToNameEmail($message->getBcc()),
                'message' => $message->getBody(),
                'subject' => $message->getSubject(),
            ],
        ];
    }

    protected function mapContactsToNameEmail($contacts)
    {
        $formatted = [];
        if (empty($contacts)) {
            return [];
        }
        foreach ($contacts as $address => $display) {
            $formatted[] =  [
                'name' => $display,
                'email' => $address,
            ];
        }
        return $formatted;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

Modify the getPayload function to send whatever payload you want to the API. Refer to Guzzle requests (opens new window) to learn how to format Guzzle requests.

# Extend Laravel's MailManager

Extend Laravel's MailManager class (opens new window) to create a method for your custom transport class.

We're going to create a createCustomTransport method similar to the createMailgunTransport method (opens new window). It will be used by the createTransport method (opens new window) to create an instance of our CustomTransport class.

In app/CustomMailDriver/CustomMailManager.php:

<?php

namespace App\CustomMailDriver;

use Illuminate\Mail\MailManager;
use App\CustomMailDriver\CustomTransport;


class CustomMailManager extends MailManager
{
    protected function createCustomTransport()
    {
        $config = $this->app['config']->get('services.custom_mail', []);

        return new CustomTransport(
            $this->guzzle($config), $config['key'], $config['url']
        );
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# Service configuration

Add a new section to config/services.php for the API's URL and authorization key:

'custom_mail' => [
    'url' => env('CUSTOM_MAIL_URL'),
    'key' => env('CUSTOM_MAIL_API_KEY')
],
1
2
3
4

# Replace Laravel's MailManager with our CustomMailManager

We will create a service provider to register our CustomMailManager instead of Laravel's MailManager in the service container.

Create a new service provider:

php artisan make:provider CustomMailServiceProvider
1

In app/Providers/CustomMailServiceProvider.php:

<?php

namespace App\Providers;

use Illuminate\Mail\MailServiceProvider;

use App\CustomMailDriver\CustomMailManager;

class CustomMailServiceProvider extends MailServiceProvider
{
    /**
     * Register the Illuminate mailer instance.
     *
     * @return void
     */
    protected function registerIlluminateMailer()
    {
        $this->app->singleton('mail.manager', function($app) {
            return new CustomMailManager($app);
        });

        // Copied from Illuminate\Mail\MailServiceProvider
        $this->app->bind('mailer', function ($app) {
            return $app->make('mail.manager')->mailer();
        });
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# Register the service provider

Finally, we need to register our new service provider in our app's config:

In config/app.php:

'providers' => [
    ...
    // Illuminate\Mail\MailServiceProvider::class,
    App\Providers\CustomMailServiceProvider::class,
],
1
2
3
4
5

Make sure to copy out Laravel's MailServiceProvider.

# Mailer Config

In config/mail.php, under mailers, you need to add a new entry:

'custom' => [
    'transport' => 'custom',
],
1
2
3

The value of transport must be the lowercase value the word between create and Transport in your new method in CustomMailManager. custom in this case because the method was createCustomTransport.

# Try it out

To try it out, we can create a Postman Mock API (opens new window).

Replace the GET request to postman-echo.com/get with a POST request to postman-echo.com/post.

Clicking on the mock in Postman will take you to a page where you can view the requests take have been sent to it.

Mock in Postman

Now we need to update our .env file:

MAIL_MAILER=custom
CUSTOM_MAIL_URL="https://bbe3d6a4-90e5-480d-aece-0090afdf4cea.mock.pstmn.io"
CUSTOM_MAIL_API_KEY="secret-key"
1
2
3

Now you can send an email from your application and view the requests that have been received:

Mock requests received

# Tests

We can create a Guzzle mock handler (opens new window) to test our new mail driver with.

Here is an example test:

<?php

namespace Tests\Unit;

use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\Psr7\Response;
use App\CustomMailDriver\CustomTransport;
use Swift_Message;
use Tests\TestCase;
use Illuminate\Support\Facades\Log;

class CustomMailDriverTest extends TestCase
{
    protected function createEmail()
    {
        $message = new Swift_Message('Test subject', '<body>Message body</body>');
        $message->setTo('[email protected]');
        $message->setCc('[email protected]');
        $message->setBcc('[email protected]');
        $message->setFrom('[email protected]');
        return $message;
    }

    protected function mockGuzzleClient()
    {
        $this->mock_handler = new MockHandler([
            new Response(200, []),
        ]);

        $this->client = new Client(['handler' => $this->mock_handler]);
    }

    public function testSendMail()
    {
        $this->mockGuzzleClient();

        $message = $this->createEmail();

        $test_url = 'https://localhost:8000/email';

        $transport = new CustomTransport(
            $this->client,
            $test_url,
            'secret-key'
        );

        $transport->send($message);

        $last_request = $this->mock_handler->getLastRequest();
        $body = (string) $last_request->getBody();
        $data = json_decode($body, true);

        $expected = [
            'to' => [
                [
                    'name' => null,
                    'email' => '[email protected]',
                ]
            ],
            'cc' => [
                [
                    'name' => NULL,
                    'email' => '[email protected]',
                ]
            ],
            'bcc' => [
                [
                    'name' => NULL,
                    'email' => '[email protected]',
                ]
            ],
            'message' => '<body>Message body</body>',
            'subject' => 'Test subject',
        ];

        // Test that the data was sent to the correct URL
        $this->assertEquals($test_url, (string)$last_request->getUri());

        // Test the correct data was sent to the API
        $this->assertEquals($expected, $data);

        // Test that the authorization key was sent in the headers
        $this->assertEquals('Bearer secret-key', $last_request->getHeaderLine('Authorization'));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

# Older Laravel versions

The mail driver methods have been changed between Laravel 6 and 7. For older versions of Laravel, I'd suggest the following blog posts:

# Repository

The code for this post is available on Github (opens new window). Any questions, comments or issues can be posted as issues on the repository.

Newsletter

If you'd like to subscribe to my blog, please enter your details below. You can unsubscribe at any time.

Powered by Buttondown.

Last Updated: 11/20/2023, 10:04:51 AM