3
votes

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

2 Answers

5
votes

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.

1
votes

After a lot of researching I stumbled upon the different queue commands. I tried queue:listen (which is not described in the Laravel 5.4 docs) instead of queue:work and the problems are gone.

Of course, this doesn't really explain the described behavior, but fortunately it doesn't matter, because I can live with this solution/workaround.

Another strange behavior is, that from time to time the queue worker throws an exception because the database was locked. No idea, when or why this happened.

This post explained a little bit, why things can happen: What is the difference between queue:work --daemon and queue:listen

In a nutshell, queue:listen solved my problem and another very strange db lock problem as well.