How to set dynamic SMTP data in Laravel 5.4 for queued emails?

Viewed 2741

In my application each user can use his own SMTP server. Therefor the config must be provided. I'm using Laravel Notifications to send the emails. If I'm using no queue (that means sync), there is no problem.

I made a CustomNotifiable Trait:

        config([
            'mail.host' => $setting->smtp_host,
            'mail.port' => $setting->smtp_port,
            'mail.username' => $setting->smtp_username,
            'mail.password' => $setting->smtp_password,
            'mail.encryption' => $setting->smtp_encryption,
            'mail.from.address' => $setting->smtp_from_address,
            'mail.from.name' => $setting->smtp_from_name,
        ]);

        (new \Illuminate\Mail\MailServiceProvider(app()))->register();

After that, I restore the original config:

        config([
            'mail' => $originalMailConfig
        ]);

        (new \Illuminate\Mail\MailServiceProvider(app()))->register();

No problem until now. But if it's queued, just the first config after starting the queue worker will be taken for all further emails, even if any other SMTP config is provided. The default config from config/mail.php will be overridden. But this only works the first time.

I've made in the AppServiceProvider::boot method (the SMTP config is stored at the notification):

    Queue::before(function (JobProcessing $event) {

        // Handle queued notifications before they get executed
        if (isset($event->job->payload()['data']['command']))
        {
            $payload = $event->job->payload();
            $command = unserialize($payload['data']['command']);

            // setting dynamic SMTP data if required
            if (isset($command->notification->setting))
            {
                config([
                    'mail.host' => $command->notification->setting->smtp_host,
                    'mail.port' => $command->notification->setting->smtp_port,
                    'mail.username' => $command->notification->setting->smtp_username,
                    'mail.password' => $command->notification->setting->smtp_password,
                    'mail.encryption' => $command->notification->setting->smtp_encryption,
                    'mail.from.address' => $command->notification->setting->smtp_from_address,
                    'mail.from.name' => $command->notification->setting->smtp_from_name,
                ]);

                (new \Illuminate\Mail\MailServiceProvider(app()))->register();
            }
        }

    });

Of course, the original config get restored:

    Queue::after(function (JobProcessed $event) use ($originalMailConfig) {

        $payload = $event->job->payload();
        $command = unserialize($payload['data']['command']);

        // restore global mail settings
        if (isset($command->notification->setting))
        {
            config([
                'mail' => $originalMailConfig
            ]);

            (new \Illuminate\Mail\MailServiceProvider(app()))->register();
        }

    });

It seems, as the Swift Mailer has a cache or something like that. I registered a new MailServiceProvider, which should simply replace the old one. So if I set the config with the new SMTP data, the new registered provider should take them. Logging the config shows even in the TransportManager, that the correct SMTP data were set, right before sending the mail, but the mail was sent with the first set config.

I found this thread and tried the linked solution, but with the same result: How to set dynamic SMTP details laravel

So I need a way to override the Services / ServiceProvider / SMTP config. Even if the Supervisor restarts the queue, there is a chance that multiple emails with different configs should be send at the same time.

2 Answers

In Laravel 5.4+, as I see that the Mailer Class is a singleton that hold a MailTransport Class, which is responsible for the config of SMTP mail and is a singleton,too; I just have to override the config using the following approach:

First, I setup a trait so I can just turn this feature on some Mails:

trait MailSenderChangeable
{
    /**
     * @param array $settings
     */
    public function changeMailSender($settings)
    {
        $mailTransport = app()->make('mailer')->getSwiftMailer()->getTransport();
        if ($mailTransport instanceof \Swift_SmtpTransport) {
            /** @var \Swift_SmtpTransport $mailTransport */
            $mailTransport->setUsername($settings['email']);
            $mailTransport->setPassword($settings['password']);
        }
    }
}

Then, in the build() method of your mail class, you can utilize the above trait and call:

    $this->changeMailSender([
        'email'=>$this->company->email,
        'password'=>$this->company->email_password,
    ]);

Boom, let the Laravel do the rest.

Related