8

I'm using the Laravel 4's Mail::queue() to send emails, using the built in Mailgun driver. The problem is that there are multiple Mailgun domains I would like to be able to send emails from, but the domain must be set in app/config/services.php. Since I'm using Mail::queue(), I can't see how to dynamically set that configuration variable.

Is there any way to do what I'm asking? Ideally, I'd like to be able to pass in the domain when I call Mail::queue() (the Mailgun api key is the same for all the domains I want to send from).

Will Durney
  • 1,168
  • 2
  • 13
  • 16

5 Answers5

9

I used Macros to add dynamic configuration. I don't remember if this can be done in Laravel 4 but works on 5.

Register macro in service provider (AppServiceProvider)

public function boot()
{
    Mail::macro('setConfig', function (string $key, string $domain) {

        $transport = $this->getSwiftMailer()->getTransport();
        $transport->setKey($key);
        $transport->setDomain($domain);

        return $this;
    });
}

Then I can use like this:

\Mail::setConfig($mailgunKey, $mailgunDomain)->to(...)->send(...)

In your case

\Mail::setConfig($mailgunKey, $mailgunDomain)->to(...)->queue(...)
Mirceac21
  • 1,741
  • 17
  • 24
5

Switching the configuration details of the Laravel Mailer at runtime is not that hard, however I don't know of any way it can be done using the Mail::queue facade. It can be done by using a combination of Queue::push and Mail::send (which is what Mail::queue does anyway).

The problem with the Mail::queue facade is that the $message parameter passed to the closure, is of type Illuminate\Mail\Message and we need to modify the mailer transport, which is only accessible through the Swift_Mailer instance (and that is readonly within the Message class).

You need to create a class responsible for sending the email, using a Mailgun transport instance that uses the domain you want:

use Illuminate\Mail\Transport\MailgunTransport;
use Illuminate\Support\SerializableClosure;

class SendQueuedMail {

    public function fire($job, $params)
    {
        // Get the needed parameters
        list($domain, $view, $data, $callback) = $params;

        // Backup your default mailer
        $backup = Mail::getSwiftMailer();

        // Setup your mailgun transport
        $transport = new MailgunTransport(Config::get('services.mailgun.secret'), $domain);
        $mailer = new Swift_Mailer($transport);

        // Set the new mailer with the domain
        Mail::setSwiftMailer($mailer);

        // Send your message
        Mail::send($view, $data, unserialize($callback)->getClosure());

        // Restore the default mailer instance
        Mail::setSwiftMailer($backup);
    }

}

And now you can queue emails like this:

use Illuminate\Support\SerializableClosure;

...

Queue::push('SendQueuedMail', ['domain.com', 'view', $data, serialize(new SerializableClosure(function ($message)
{
    // do your email sending stuff here
}))]);

While it's not using Mail::queue, this alternative is just as compact and easy to read. This code is not tested but should work.

Bogdan
  • 43,166
  • 12
  • 128
  • 129
  • This looks great. But I'm getting `exception 'InvalidArgumentException' with message 'Callback is not valid.' in /Illuminate/Mail/Mailer.php:370` – Will Durney Feb 26 '15 at 14:45
  • Looks like Mail::queue serializes the callback closure and unserializes it when it runs the job. Since I'm not doing that, the callback just turns into an empty array. – Will Durney Feb 26 '15 at 15:22
  • Yes, you're right, the serialization is needed because the payload passed to the queue is encoded as JSON and the closure would need to be serialized, so that it can be passed as a JSON property value. I'll update my answer in a few minutes with a fix for that. – Bogdan Feb 26 '15 at 15:28
  • I've updated my answer so that it passes a serialized closure. That should fix the issue, although it makes the queue push code a little less readable. If you'd like to make it more readable you could create a custom [Facade](http://laravel.com/docs/4.2/facades#creating-facades). – Bogdan Feb 26 '15 at 15:39
  • Yeah, I did that. I also added: `if (Config::get('mail.driver') !== 'mailgun') { Mail::send($view, $data, $callback); return; }` so that I can use the same facade for all emails even when using a testing enviroment. – Will Durney Feb 26 '15 at 15:54
  • Note for Laravel 5 users, use [SuperClosure\Serializer](https://github.com/jeremeamia/super_closure) instead of `SerializableClosure` – Daniel Buckmaster Jun 07 '18 at 14:03
5

This works in Laravel 5.4:

// Get the existing SwiftMailer
$swiftMailer = Mail::getSwiftMailer();

// Update the domain in the transporter (Mailgun)
$transport = $swiftMailer->getTransport();
$transport->setDomain('YOUR-DOMAIN.HERE');

// Use the updated version
$mailer = Swift_Mailer::newInstance($transport);
Mail::setSwiftMailer($mailer);
  • For Laravel 5.6 (or may be above as well) it works by changing the line of `$mailer = Swift_Mailer::newInstance($transport);` to `$mailer = new \Swift_Mailer($transport);` as mentioned against this: https://github.com/yiisoft/yii2/issues/7848#issue-63849469 – Umair Malhi Apr 11 '19 at 08:27
2

My use case was similar to this, in short I just wanted to automatically configure the mailgun sending domain at runtime, by looking at the domain set in the from address field of the message (which I set on the fly before sending using Mail::from(...)->send(...)). This would solve the OP's use case if they are setting the from address in the message to match the mailgun sending domain, which likely should be done.

My solution registers an alternate MailgunTransport which overrides the built in MailgunTransport and sets the domain before sending. This way I only need to register the new driver in my mail.php, and call Mail::send or Mail::queue.

config\mail.php:

'driver' => env('MAIL_DRIVER', 'mailgun-magic-domain')

providers\MailgunMagicDomainProvider:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

use Illuminate\Mail\Transport\MailgunTransport;
use Swift_Mime_Message;
use Illuminate\Support\Arr;

use GuzzleHttp\Client as HttpClient;

class MailgunMagicDomainProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        $swiftTransport = $this->app['swift.transport'];

        $swiftTransport->extend('mailgun-magic-domain', function($app) {
            $config = $app['config']->get('services.mailgun', []);

            $client = new HttpClient(Arr::add(
                Arr::get($config, 'guzzle', []), 'connect_timeout', 60
            ));

            return new MailgunTransportWithDomainFromMessage(
                $client, 
                $config['secret'], 
                $config['domain'] // <- we have to pass this in to avoid re-writing the whole transport, but we'll be dynamically setting this before each send anyway
            );
        });
    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {

    }
}

/**
 * Overrides the built in Illuminate\Mail\Transport\MailgunTransport but doesnt pull the
 * mailgun sending domain from the config, instead it uses the domain in the from address 
 * to dynamically set the mailgun sending domain
 */
class MailgunTransportWithDomainFromMessage extends MailgunTransport 
{
    /**
     * {@inheritdoc}
     */
    public function send(Swift_Mime_Message $message, &$failedRecipients = null)
    {
        $this->setDomain($this->getDomainFromMessage($message));

        return parent::send($message, $failedRecipients);
    }

    protected function getDomainFromMessage(Swift_Mime_Message $message) 
    {
        $fromArray = $message->getFrom();

        if (count($fromArray) !== 1) {
            throw new \Exception('Cannot use the mailgun-magic-domain driver when there isn\'t exactly one from address');
        }

        return explode('@', array_keys($fromArray)[0])[1];
    }
}

config/app.php:

'providers' => [
    ...
    \App\Providers\MailgunMagicDomainProvider::class
],
AaronHS
  • 1,334
  • 12
  • 29
  • Thanks! That's the smart solution I was looking for. One thing: `$swiftTransport = $this->app['swift.transport'];` is not correct, at least not in Laravel 8. You should be getting the mail manager there, so the code should be `$mailManager = $this->app['mail.manager'];` Looking into Illuminate\Mail\MailManager->extend() is a good starting point to get a grasp of how this all works. I also needed to get the configuration instance differently and pass the endpoint to the constructor of the custom transport. – Leif Jan 20 '22 at 10:37
0

Maybe it's useful to somebody, I solved it as follows;

In a ServiceProvider under the boot function/method;

public function boot()
{
    Mail::macro('setConfig', function (string $key, string $domain) {

        config()->set('services', array_merge(config('services'), [
            'mailgun' => [
                'domain' => $domain,
                'secret' => $key
            ]
        ]));
    });
}

Call to queue

Mail::setConfig($key, $domain)->to(...)->queue(...)
JON
  • 965
  • 2
  • 10
  • 28