Order confirmations and password resets are built in, but many stores need their own emails: a notification when a customer asks a product question, a reminder for quote approvals, or an alert to the warehouse. Magento's email framework handles templates, translations, store-specific senders and admin-editable content.

Step 1: Register the Template

etc/email_templates.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Email:etc/email_templates.xsd">
    <template id="mageservices_faq_question_notification"
              label="New Customer Question"
              file="question_notification.html"
              type="html"
              module="MageServices_Faq"
              area="frontend"/>
</config>

Step 2: Create the Template File

The comment blocks at the top define the subject and the variables shown in the admin template editor. Use the store's header and footer templates so the email matches your other transactional emails.

view/frontend/email/question_notification.html
<!--@subject {{trans "New question about %product_name" product_name=$product_name}} @-->
<!--@vars {
"var customer_name":"Customer Name",
"var product_name":"Product Name",
"var question":"Question"
} @-->

{{template config_path="design/email/header_template"}}

<p>{{trans "Hello,"}}</p>
<p>{{trans "%customer_name asked a question about %product_name:" customer_name=$customer_name product_name=$product_name}}</p>
<blockquote>{{var question}}</blockquote>
<p>{{trans "Reply from the admin panel under Content > FAQs."}}</p>

{{template config_path="design/email/footer_template"}}
Pass scalar values (strings and numbers) as template variables. Since Magento 2.4, email templates use strict mode, where calling arbitrary methods on objects inside {{var}} is restricted.

Step 3: A Sender Service

Model/QuestionNotifier.php
<?php
declare(strict_types=1);

namespace MageServices\Faq\Model;

use Magento\Framework\App\Area;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Mail\Template\TransportBuilder;
use Magento\Framework\Translate\Inline\StateInterface;
use Psr\Log\LoggerInterface;

class QuestionNotifier
{
    private const TEMPLATE_ID = 'mageservices_faq_question_notification';

    public function __construct(
        private readonly TransportBuilder $transportBuilder,
        private readonly StateInterface $inlineTranslation,
        private readonly LoggerInterface $logger
    ) {
    }

    public function notify(
        int $storeId,
        string $recipientEmail,
        string $customerName,
        string $productName,
        string $question
    ): bool {
        $this->inlineTranslation->suspend();

        try {
            $transport = $this->transportBuilder
                ->setTemplateIdentifier(self::TEMPLATE_ID)
                ->setTemplateOptions(['area' => Area::AREA_FRONTEND, 'store' => $storeId])
                ->setTemplateVars([
                    'customer_name' => $customerName,
                    'product_name' => $productName,
                    'question' => $question,
                ])
                ->setFromByScope('general', $storeId)
                ->addTo($recipientEmail)
                ->getTransport();

            $transport->sendMessage();

            return true;
        } catch (LocalizedException $e) {
            $this->logger->error('Could not send question notification', ['exception' => $e]);

            return false;
        } finally {
            $this->inlineTranslation->resume();
        }
    }
}

setFromByScope('general', $storeId) uses the "General Contact" identity from Stores → Configuration → General → Store Email Addresses. Use sales, support, custom1 or custom2 for other identities.

Step 4: Let Admins Choose the Template

Add a field to your module's configuration so merchants can override the template in Marketing → Email Templates and select it:

<field id="question_template" translate="label" type="select" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1">
    <label>Question Notification Template</label>
    <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model>
</field>

Then read the configured template ID with ScopeConfigInterface and pass it to setTemplateIdentifier(), falling back to the default ID. Our system.xml tutorial covers configuration fields in detail.

Sending From Cron or the CLI

Email templates need area configuration. Inside cron jobs and CLI commands, wrap the call in area emulation: inject Magento\Store\Model\App\Emulation and call startEnvironmentEmulation($storeId, Area::AREA_FRONTEND, true) before sending and stopEnvironmentEmulation() afterwards.

Testing Emails Safely

  • Use a mail catcher such as Mailpit in local Docker environments so no email reaches real customers.
  • Check Stores → Configuration → Advanced → System → Mail Sending Settings: email communication must be enabled.
  • For production, send through a proper SMTP or email API provider with SPF, DKIM and DMARC configured.
  • For high volumes, enable asynchronous sending for sales emails and send custom emails from a queue consumer.

Triggering the Email From an Event

A common pattern is to send the email from an observer or, better for performance, from a queue consumer. For example, when a customer submits a question, your controller saves it and publishes a message; a consumer then calls QuestionNotifier::notify(). The customer gets a fast response, and email delivery problems never break the storefront request.

If you send from an observer, keep it short and catch exceptions so a mail server problem cannot interrupt the customer's action. Log failures and consider a retry mechanism for important notifications.

Frequently Asked Questions

Why is my Magento email not sent?

Check that email communication is enabled, the template ID matches email_templates.xml, var/log/exception.log for errors, and your server or SMTP module can send mail.

Can I attach a PDF to a custom email?

TransportBuilder does not support attachments out of the box. Use a well-maintained module or a plugin that adds a MIME part, or link to a secure download instead.

How do I translate the email?

Use {{trans}} in the template and add translations to your module or theme i18n CSV files. The store passed in setTemplateOptions determines the locale.

🚀

Need help with this on your store?

Our Magento engineers can implement it for you, review your code or take on the whole project.

Explore Magento Extension Development
MS
About the author

Written by the Magento Services engineering team — Magento 2, Adobe Commerce and Hyvä specialists since 2014. We write about problems we solve on real client stores.