1

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.

Innit2
  • 107
  • 1
  • 7

6 Answers6

3

Laravel 9.x is using symfony/mailer that might cause this conflict. Instead, you can use the mailer() option to change the mail driver on the fly.

Mail::mailer('postmark')
        ->to($request->user())
        ->send(new OrderShipped($order));

You can find the details on the documentation.

Updated

I checked the symfony/mailer documentation and i guess this is what you want. You can build your own transport on the fly with dynamic SMTP details as well.

use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mime\Email;

$transport = Transport::fromDsn('smtp://localhost');
$mailer = new Mailer($transport);

$email = (new Email())
    ->from('hello@example.com')
    ->to('you@example.com')
    //->cc('cc@example.com')
    //->bcc('bcc@example.com')
    //->replyTo('fabien@example.com')
    //->priority(Email::PRIORITY_HIGH)
    ->subject('Time for Symfony Mailer!')
    ->text('Sending emails is fun again!')
    ->html('<p>See Twig integration for better HTML integration!</p>');

$mailer->send($email);
jogesh_pi
  • 9,762
  • 4
  • 37
  • 65
  • Thank you, I did read the documentation and if I understood correctly then I have to edit mail.php file everytime I want to edit the email configurations. I hope I am wrong but this solution isn't very dynamic for my use case. – Innit2 Apr 19 '22 at 12:30
  • @Innit2 please check the updated answer, maybe this can help you to make more dynamic mailer. – jogesh_pi Apr 19 '22 at 13:02
  • No, sorry this did not help me, thank you though! I did find another solution and I will answer my own question soon. – Innit2 Apr 21 '22 at 13:48
  • Hi @Innit2! I'm facing the same problem and I'm curious about your solution. I hope you can find some time to answer your own question soon. :) – Kalo Nov 10 '22 at 13:42
  • Hello @Kalo, I answered my own question. I hope it helps you :) – Innit2 Nov 15 '22 at 19:07
  • @jogesh_pi, this works, is there a way to insert Laravel email class with this mailer? – Amit Feb 23 '23 at 13:33
  • @jogesh_pi, Is there a way to add Markup Mail class to this mailer? – Amit Feb 24 '23 at 10:23
2

This works for me...

In your config/mail.php file you can define the different accounts as follows:

'mailers' => [
        'smtp' => [
            'transport' => 'smtp',
            'host' => 'smtp.google',
            'port' => 465,
            'encryption' => 'ssl',
            'username' => 'youraccount@something.com',
            'password' => 'password',
            'timeout' => null,
            'local_domain' => env('MAIL_EHLO_DOMAIN'),
        ],
        'OtherMailer' => [
            'transport' => 'smtp',
            'host' => 'smtp.gmail.com',
            'port' => '465',
            'encryption' => 'ssl',
            'username' => 'otheraccount@something.com',
            'password' => 'password',
            'timeout' => null,
            'local_domain' => env('MAIL_EHLO_DOMAIN'),
        ],

Next, in your controller or model, you can set which mailer are you going to use.

Mail::mailer('otherMailer')->to('person@something.com')->send(new SendLetter($sendMail));

Hope this helps.

sikios182
  • 33
  • 9
2

None of these solutions worked for me on production (Forge). For some reason it kept on using the FROM_USERNAME specified in the Environment, eventhough I tried to overwrite it before sending the E-mail. I even upgraded from Laravel V8 to V9, because the mail system had been reworked, but that didn't work either.

My solution is not the best, but atleast it works.

Save the user specific SMTP credentials in the User table.

Generate a Mailable class (https://laravel.com/docs/10.x/mail#generating-mailables) and use it as the first parameter when calling the sendEmail function. The envelope() function is pretty redundant in the Mailable class, so you can just avoid creating it. Just use the content() function.

User.php

use Symfony\Component\Mailer\Transport\Dsn;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransportFactory;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use Illuminate\Mail\Mailable;

public function sendEmail(Mailable $mailable, string $subject, string $to, string $cc = "")
{
    $email = (new Email())->html($mailable->render())->to($to)->subject($subject)->from(new Address($this->mail_from_address, $this->name));

    $mailer = new \Symfony\Component\Mailer\Mailer((new EsmtpTransportFactory)
                ->create(new Dsn('smtp', $this->mail_host, $this->mail_username, Crypt::decryptString($this->mail_password), $this->mail_port)));

    $mailer->send($email);
}

Send E-mail by doing this:

// send mail (using the User's SMTP credentials)
$user->sendEmail(new OrderShipped($this->order), "Subject", "to@example.com");

OrderShipped is a Mailable type.

Unicco
  • 2,466
  • 1
  • 26
  • 30
  • To me, it says 'Argument #1 ($mailable) must be of type App\Http\Controllers\Mailable,'. did you use any class for 'Mailable $mailable'? – Amit Feb 23 '23 at 13:13
  • This works. It's a good solution – Amit Feb 26 '23 at 04:00
0

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'=> 'test@test.test'
    '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.

Innit2
  • 107
  • 1
  • 7
  • Thanks for sharing your solution! Actually, a few hours ago I managed to solve my problem using this answer: [link](https://stackoverflow.com/a/70155305/12304742) – Kalo Nov 16 '22 at 21:07
0

Thanks man, worked for me. Here is code of jobs file

$config = get_email_configuration($company->id);
        
$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' => 
            [
                'address' => $config['from']['address'],
                'name' => $config['from']['name'],
            ],
        'mail.mailers.' . $temp_config_name . '.auth_mode' => true,
    ]);

Mail::mailer($temp_config_name)
    ->to($this->supervisorEmail)
    ->bcc($this->admin)
    ->send($email);
0

In Laravel 9+, I was able to use different smtp settings for every user, based on his/her credentials using the Symfony Mailer components, and reusing Laravel Mailable functionality like this:

$data = [
        'name' => $name,
        'document' => $document,
        'generated_document' => $generated_document
    ];
    $password = $user_pass; //Obtained from DB
    $user_name = $user_name; //Obtained from DB
    $mailer = Mail::mailer('smtp');
    //Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport
    $transport = new EsmtpTransport( env('MAIL_HOST', 'localhost'), 465); 
    $transport->setUsername($user_name)->setPassword($password);
    $mailer->setSymfonyTransport($transport); //Using the component with the credentials for the current user
    $mailer->to($email)->send(new SendSignedPDF($data)); //Sending the email using the Laravel facade plus a custom mailable

Hope it helps someone.

GAlonso
  • 516
  • 5
  • 7