Search code examples
laravelsymfonyemaildynamiclaravel-9

Laravel 9 dynamic Email configurations


I am coming to you with a problem to which I couldn't find a solution on google after hours of googling.

I want to be able to send emails by using different SMTP email configurations which I can add or change at runtime. I am building a website which hosts a lot of projects for a lot of clients and we need be able to send emails on their behalf. I know I can set up different configurations in the .env file but that solution is not good enough because I want to keep the configurations in the database where they can be easily queried/updated etc.

One solution is to use this method from this tutorial. It uses Swift mailer to make a method which returns a new mailer object but this doesn't seem to work in Laravel 9. Apparently Swift mailer is no longer maintained and has been succeeded by Symfony Mailer. Unfortunately I couldn't find a way to use the new Symfony Mailer in the way that I just described, although I'd certainly prefer it if I could get it working.

I wonder if it's possible to use that same method with Symfony Mailer? Here's the error that I get when I use the same code as in the tutorial:

Class "Swift_SmtpTransport" not found

I added the class to the namespace and I also changed the syntax from new Swift_SmtpTransport to \Swift_SmtpTransport::newInstance but that did not resolve the error.

If anyone has any ideas/suggestions then I would highly appreciate it! I really did not expect such a simple thing to be so difficult.


Solution

  • Backstory

    In my company website, almost every user(employee) has their own email configuration and some models also have their own email configuration. All configurations are saved to a database table so they could be changed/added during runtime. All emails are sent to queue and they are processed by queue worker.

    I will try to provide a clean example of how I managed to change configuration during runtime. I cropped out most of the unnecessary code and changed some variables and text to make it easily readable.

    Logic

    For example, if I wish that an email is sent out from authenticated user's email, after they submit a certain form, then I will do it like so:

    $email_data = array(
        'header'=>__('Info') , 
        'subheader'=>__('This is an automated email. Do not reply.'),
        'mail_to'=> '[email protected]'
        'content'=>   ...
         );
        
        sendEmailFromConfig(new mailTemplate($email_data), $auth_user->email_config_id);
    

    The sendEmailFromConfig is function is from helper class so it can be called from anywhere. For the first argument I pass in mailTemplate, which derives from mailable, with the custom data that I want it to use. The the second argument is email configuration id.

    The mailtemplate class:

    class mailTemplate extends Mailable
    {
        use Queueable, SerializesModels;
    
        public $data;
        
        public function __construct($data)
        {
            $this->data = $data; 
    
        }
    
        public function build()
        {   
            // optional data (for view)
            $email_data['url'] = $this->data['url'];
            
            $email_data['data1'] = $this->data['data1'];
            $email_data['data2'] = $this->data['data2'];
            $email_data['data3'] = $this->data['data3'];
    
    
            // required
            $email_data['view_name'] = 'emails.content.mailTemplate'; 
            $email_data['reply_to'] = isset($this->data['reply_to']) ? $this->data['reply_to'] : '';
            $email_data['cc'] = [];
            $email_data['bcc'] = [];
            $email_data['email_to'] = isset($this->data['email_to']);
            $email_data['email_subject'] = $this->data['email_subject'];
            
            
            logEmail($this->data); // Another helper function to log sent emails
            
            return $this
                ->subject($email_data['email_subject'])
                ->to($email_data['email_to'])
                ->cc($email_data['cc'])
                ->bcc($email_data['bcc'])
                ->replyTo($email_data['reply_to'])
                ->view($email_data['view_name'], ['email_data' => $email_data]);
        }
        
    }
    

    The sendEmailFromConfig function, among other unrelated things, just generates a new job like this:

    function sendEmailFromConfig($data, $config_id) {
        $config = EmailConfiguration::find($config_id); 
        dispatch(new SendEmailJob($data, $config));
    }
    

    Notice, the $data value comes from the mailable which was passed as first the argument.

    The SendEmailJob syntax is like any other job you can find in laravel documentation, but what makes the magic happen is this:

        $temp_config_name = 'smtp_rand_' . str::random(5); // every email is sent from different config to avoid weird bugs
        
        config()->set([
            'mail.mailers.' . $temp_config_name . '.transport' => 'smtp',
            'mail.mailers.' . $temp_config_name . '.host' => $config->host,
            'mail.mailers.' . $temp_config_name . '.port' => $config->port,
            'mail.mailers.' . $temp_config_name . '.username' => $config->username,
            'mail.mailers.' . $temp_config_name . '.password' => $config->password,
            'mail.mailers.' . $temp_config_name . '.encryption' => $config->encryption,
            'mail.mailers.' . $temp_config_name . '.from' => 
            [
                //FIXME TWO BOTTOM LINES MUST BE GIVEN A DO OVER PROBABLY
                'address' => $config->from_address,
                'name' => $config->from_name),
            ],
            'mail.mailers.' . $temp_config_name . '.auth_mode' => $config->auth_mode,
            ]);
    
    Mail::mailer($temp_config_name)->send($data); // sends email
    

    This sets a new config in cache just before sending the job to the worker service (which handles queues). This should also work without queues - in that case you should first try without the $temp_config_name variable.

    This solution might be considered wrong and it's definitely not pretty but this is the only way that I managed to get it working properly. Notice how the $temp_config_name is changed in every new job, even if same data is being sent from same email config - this fixed a bug. The bug was that, after first successful email from a configuration, the next email wouldn't be sent out. I don't know why this bug happened, but setting a different config name every time fixed the issue.

    I should mention that these temporary configs will start piling up in the cache every time you send an email. I haven't had the time to find a good solution to this yet, if anyone does know what to do, then please tell (or if you have a better solution than what I'm doing). I know that restarting the worker service will automatically delete these temporary configs. I guess one way would be to to restart the worker service after every x jobs.

    I also want to say that I'm a beginner in PHP and Laravel and I do think that there might be a better solution out there, I just wasn't able to find it. I also want to say that I left out a lot of code (for example some try catches, logging function calls, some applicaiton specific functionality etc ..), I just wanted to show the core logic.