Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I change SMTP details globally at runtime?

I'm using Laravel 5.5. The nature of the website is a 'multisite' architecture where multiple websites/domains are run from the same codebase.

I've come across an issue when sending email. I need to change the from name and address as well as the transport (SMTP, etc) options depending on which website is being viewed. I have these details stored in a config file.

The easiest way is to just pull those details in the Controller before I call Mail::send/Mail::queue and to update them. However, this brings back 2 issues:

  1. There is a heavy reliance on remembering to actually do that every time I send any email in the code. In short, it's not abiding by DRY.
  2. I'd be forced to use Mail::send instead of Mail::queue, because the queue wouldn't have any idea of the config update from the time it was queued only from when it is processed .

How can I achieve what I am looking to do here in a clean way?

I thought about extending all of my 'Mailable' classes with a custom class that updates the SMTP details, but it doesn't look like you can update the SMTP/Transport information after the class is initiated; you can only update the from name and address.

like image 506
Mike Avatar asked Nov 29 '25 16:11

Mike


1 Answers

I managed to find a way to do this.

I had my mailable class (ContactFormMailable) extend a custom class, as follows:

<?php

namespace CustomGlobal\Mail;

use CustomGlobal\Mail\CustomMailable;
use CustomGlobal\ContactForm;

class ContactFormMailable extends CustomMailable
{
    public $contact_form;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct(ContactForm $contact_form)
    {
        $this->contact_form = $contact_form;
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        $view = $this->get_custom_mail_view('contact_form', $this->contact_form);

        return $this->subject('Contact Form Enquiry')
            ->view($view);
    }
}

You'll notice I'm calling get_custom_mail_view. This is in my extended class and used to calculate the view and template I need to use for my mail, depending on the website being viewed. In here I also set the location of my config folder.

<?php

namespace CustomGlobal\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
use Swift_Mailer;
use Swift_SmtpTransport;
use CustomGlobal\Website;
use CustomGlobal\Territory;

class CustomMailable extends Mailable
{
    use Queueable, SerializesModels;

    public $layout_view_to_serve;
    public $host_folder;

    /**
     * Override Mailable functionality to support per-user mail settings
     *
     * @param  \Illuminate\Contracts\Mail\Mailer  $mailer
     * @return void
     */
    public function send(Mailer $mailer)
    {
        app()->call([$this, 'build']);

        $config = config($this->host_folder .'.mail');
        // Set SMTP details for this host
        $host = $config['host'];
        $port = $config['port'];
        $encryption  = $config['encryption'];

        $transport = new Swift_SmtpTransport( $host, $port, $encryption );
        $transport->setUsername($config['username']);
        $transport->setPassword($config['password']);
        $mailer->setSwiftMailer(new Swift_Mailer($transport));

        $mailer->send($this->buildView(), $this->buildViewData(), function ($message) use($config) {
            $message->from([$config['from']['address'] => $config['from']['name']]);
            $this->buildFrom($message)
                 ->buildRecipients($message)
                 ->buildSubject($message)
                 ->buildAttachments($message)
                 ->runCallbacks($message);
        });
    }

    /**
     * Calculate the template we need to serve.
     * $entity can be any object but it must contain a 
     * $website_id and $territory_id, as that is used
     * to calculate the path.
     */
    public function get_custom_mail_view($view_filename, $entity)
    {
        if(empty($view_filename)) {
            throw new Exception('The get_custom_mail_view method requires a view to be passed as parameter 1.');
        }

        if(empty($entity->website_id) || empty($entity->territory_id)) {
            throw new Exception('The get_custom_mail_view method must be passed an object containing a website_id and territory_id value.');
        }

        // Get the website and territory
        $website = Website::findOrFail($entity->website_id);
        $territory = Territory::findOrFail($entity->territory_id);

        $view_to_serve = false;
        $layout_view_to_serve = false;

        // Be sure to replace . with _, as Laravel doesn't play nice with dots in folder names
        $host_folder = str_replace('.', '_', $website->website_domain);
        $this->host_folder = $host_folder; // Used for mail config later

        /***
            Truncated for readability.  What's in this area isn't really important to this answer.
        ***/

        $this->layout_view_to_serve = $layout_view_to_serve;

        return $view_to_serve;
    }
}

It's important to remember that mail can be queued. If you do this is another way, such as setting a config at runtime, then you'll find that the process that runs the queue has no visibility/scope of your runtime config changes, and you'll end up firing out email from your default values.

I found a few answers similar to this one, which helped me out, but none of them worked completely, and some are out-dated (Swift_SmtpTransport is changed considerably since those answers).

Hopefully this helps someone else out.

like image 82
Mike Avatar answered Dec 02 '25 07:12

Mike



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!