HowTo: Send Emails Programmatically in Magento 2


Sooner or later every project needs to send an email that Magento does not send out of the box, a notification to an admin, a reminder to a customer, a report to the warehouse. The good news is that Magento gives us a clean service to do it, the TransportBuilder. Let me show you the setup I use.

This post assumes that there is already a custom module created in app/code/Vendor/Module and that you have registered an email template. If you need the template part, look at HowTo: Create Custom Email Templates in Magento 2.

The service

I like to wrap the whole thing in a small service so the calling code stays clean and testable.

php
<?php
/** app/code/Vendor/Module/Model/EmailSender.php */
namespace Vendor\Module\Model;

use Magento\Framework\Mail\Template\TransportBuilder;
use Magento\Framework\Translate\Inline\StateInterface;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Framework\App\Area;

class EmailSender
{
    /**
     * @var TransportBuilder
     */
    private $transportBuilder;

    /**
     * @var StateInterface
     */
    private $inlineTranslation;

    /**
     * @var StoreManagerInterface
     */
    private $storeManager;

    public function __construct(
        TransportBuilder $transportBuilder,
        StateInterface $inlineTranslation,
        StoreManagerInterface $storeManager
    ) {
        $this->transportBuilder = $transportBuilder;
        $this->inlineTranslation = $inlineTranslation;
        $this->storeManager = $storeManager;
    }

    /**
     * @param string $toEmail
     * @param array  $templateVars
     */
    public function send($toEmail, array $templateVars = [])
    {
        $storeId = $this->storeManager->getStore()->getId();

        $this->inlineTranslation->suspend();
        try {
            $transport = $this->transportBuilder
                ->setTemplateIdentifier('vendor_module_email_template')
                ->setTemplateOptions(['area' => Area::AREA_FRONTEND, 'store' => $storeId])
                ->setTemplateVars($templateVars)
                ->setFromByScope('general')
                ->addTo($toEmail)
                ->getTransport();

            $transport->sendMessage();
        } finally {
            $this->inlineTranslation->resume();
        }
    }
}

A few things worth pointing out:

  1. setTemplateIdentifier is the id you gave the template in email_templates.xml.
  2. Always suspend() the inline translation before sending and resume() after, otherwise you can get the translation markup leaking into the email. Doing it in a finally block guarantees it resumes even if sending throws.
  3. setFromByScope('general') uses the store’s general contact, but you can pass your own ['email' => ..., 'name' => ...].

Using it

From a controller, an observer or another service, it is now a one liner:

php
$this->emailSender->send('customer@example.com', ['customer_name' => 'John', 'order_id' => '000000123']);

Those variables are the ones you reference in the template with {{var customer_name}}.

Hope this helps.

If you have any question or know a better way to achieve this, feel free to share in the comments below.

Related links: