Sending automated emails is a core feature of any e-commerce platform, but you may have noticed a significant limitation in the default Magento 2 framework. While the platform handles transactional emails gracefully, it does not provide an out-of-the-box method to send emails with attachments.
Whether you need to send a PDF invoice, a downloadable user manual, or a dynamic report, you will need to extend the native functionality. In this guide, we will explore several proven methods to implement Magento 2 email attachments, ranging from extending the TransportBuilder to using modular helper classes.
Understanding the Magento 2 Mail Architecture
Magento 2 uses the \Magento\Framework\Mail\Template\TransportBuilder class to assemble email data. Historically, this was built on top of the Zend Framework (ZF1). However, as the platform evolved—specifically starting with Magento 2.2.7 and moving into 2.3 and 2.4—the underlying mail components shifted toward Zend Framework 2 (now Laminas).
Because the core TransportBuilder lacks an addAttachment() method, you have two primary options: override the core class globally or create a custom service to handle the attachment logic manually.
Method 1: Extending the TransportBuilder (Standard Approach)
This is the most common approach. By extending the TransportBuilder, you make the addAttachment method available wherever you inject your custom class.
For Older Magento 2 Versions (Pre-2.2.7)
In earlier versions, you could simply extend the builder and interact with the message object directly using Zend Framework 1 syntax.
<?php
namespace Your\CustomModule\Magento\Mail\Template;
class TransportBuilder
extends \Magento\Framework\Mail\Template\TransportBuilder
{
public function addAttachment(
$body,
$mimeType = Zend_Mime::TYPE_OCTETSTREAM,
$disposition = Zend_Mime::DISPOSITION_ATTACHMENT,
$encoding = Zend_Mime::ENCODING_BASE64,
$filename = null
) {
$this->message->createAttachment($body, $mimeType, $disposition, $encoding, $filename);
return $this;
}
}
For Magento 2.3.x and 2.4.x
As of Magento 2.2.7, \Magento\Framework\Mail\Message no longer extends \Zend_Mail. You must now use \Zend\Mime\Part and \Zend\Mime\Message to construct the body parts correctly.
<?php
namespace Your\CustomModule\Magento\Mail\Template;
use Magento\Framework\Mail\MessageInterface;
use Magento\Framework\Mail\MessageInterfaceFactory;
use Magento\Framework\Mail\Template\FactoryInterface;
use Magento\Framework\Mail\Template\SenderResolverInterface;
use Magento\Framework\Mail\TransportInterfaceFactory;
use Magento\Framework\ObjectManagerInterface;
use Zend\Mime\Mime;
use Zend\Mime\Part as MimePart;
use Zend\Mime\PartFactory as MimePartFactory;
use Zend\Mime\Message as MimeMessage;
use Zend\Mime\MessageFactory as MimeMessageFactory;
class TransportBuilder extends \Magento\Framework\Mail\Template\TransportBuilder
{
/** @var MimePart[] */
private $parts = [];
/** @var MimeMessageFactory */
private $mimeMessageFactory;
/** @var MimePartFactory */
private $mimePartFactory;
public function __construct(
FactoryInterface $templateFactory,
MessageInterface $message,
SenderResolverInterface $senderResolver,
ObjectManagerInterface $objectManager,
TransportInterfaceFactory $mailTransportFactory,
MimePartFactory $mimePartFactory,
MimeMessageFactory $mimeMessageFactory,
MessageInterfaceFactory $messageFactory = null
) {
parent::__construct(
$templateFactory,
$message,
$senderResolver,
$objectManager,
$mailTransportFactory,
$messageFactory
);
$this->mimePartFactory = $mimePartFactory;
$this->mimeMessageFactory = $mimeMessageFactory;
}
protected function prepareMessage()
{
parent::prepareMessage();
$mimeMessage = $this->getMimeMessage($this->message);
foreach ($this->parts as $part) {
$mimeMessage->addPart($part);
}
$this->message->setBody($mimeMessage);
return $this;
}
public function addAttachment(
$body,
$mimeType = Mime::TYPE_OCTETSTREAM,
$disposition = Mime::DISPOSITION_ATTACHMENT,
$encoding = Mime::ENCODING_BASE64,
$filename = null
) {
$this->parts[] = $this->createMimePart($body, $mimeType, $disposition, $encoding, $filename);
return $this;
}
private function createMimePart(
$content,
$type = Mime::TYPE_OCTETSTREAM,
$disposition = Mime::DISPOSITION_ATTACHMENT,
$encoding = Mime::ENCODING_BASE64,
$filename = null
) {
/** @var MimePart $mimePart */
$mimePart = $this->mimePartFactory->create(['content' => $content]);
$mimePart->setType($type);
$mimePart->setDisposition($disposition);
$mimePart->setEncoding($encoding);
if ($filename) {
$mimePart->setFileName($filename);
}
return $mimePart;
}
private function getMimeMessage(MessageInterface $message)
{
$body = $message->getBody();
if ($body instanceof MimeMessage) {
return $body;
}
/** @var MimeMessage $mimeMessage */
$mimeMessage = $this->mimeMessageFactory->create();
if ($body) {
$mimePart = $this->createMimePart((string)$body, Mime::TYPE_TEXT, Mime::DISPOSITION_INLINE);
$mimeMessage->setParts([$mimePart]);
}
return $mimeMessage;
}
}
After creating the class, you must register it in your module's etc/di.xml file to tell Magento to use your implementation:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="\Magento\Framework\Mail\Template\TransportBuilder"
type="\Your\CustomModule\Magento\Mail\Template\TransportBuilder" />
</config>
Method 2: The Helper Approach (Modular & Safe)
If you prefer not to override the global TransportBuilder—which can sometimes lead to conflicts with other third-party extensions—you can use a Helper class. This approach builds the transport, then manually injects the attachment into the transport's message object before sending.
This is particularly useful for Magento 2.3+ environments where you want to keep your code decoupled from core overrides.
<?php
amespace Vendor\Module\Helper;
use Magento\Framework\App\Area;
use Magento\Framework\App\Helper\AbstractHelper;
use Magento\Framework\App\Helper\Context;
use Magento\Framework\DataObject;
use Magento\Framework\Filesystem\Io\File;
use Magento\Framework\Mail\Template\TransportBuilder;
use Magento\Framework\Mail\TransportInterface;
use Magento\Store\Model\StoreManagerInterface;
use Zend_Mime;
use Zend\Mime\Part;
class Mail extends AbstractHelper
{
protected $transportBuilder;
protected $storeManager;
protected $file;
public function __construct(
Context $context,
TransportBuilder $transportBuilder,
StoreManagerInterface $storeManager,
File $file
) {
parent::__construct($context);
$this->transportBuilder = $transportBuilder;
$this->storeManager = $storeManager;
$this->file = $file;
}
public function send(DataObject $templateParams, array $attachments = [])
{
$storeId = $this->storeManager->getStore()->getId();
$transport = $this->transportBuilder
->setTemplateOptions(['area' => Area::AREA_FRONTEND, 'store' => $storeId])
->setTemplateIdentifier('your_email_template_id')
->setTemplateVars($templateParams->toArray())
->setFrom('general')
->addTo('[email protected]')
->getTransport();
foreach ($attachments as $a) {
$transport = $this->addAttachment($transport, $a);
}
$transport->sendMessage();
}
protected function addAttachment(TransportInterface $transport, array $file): TransportInterface
{
$part = new Part($this->file->read($file['tmp_name']));
$part->disposition = Zend_Mime::DISPOSITION_ATTACHMENT;
$part->encoding = Zend_Mime::ENCODING_BASE64;
$part->filename = $file['name'];
$transport->getMessage()->getBody()->addPart($part);
return $transport;
}
}
Method 3: Direct Zend_Mail Implementation
For developers who need a quick solution and don't require the Magento template engine for a specific task, you can use the Zend library directly. While this bypasses Magento's template variables and configurations, it is highly reliable for simple scripts.
$mail = new \Zend_Mail('utf-8');
$mail->setFrom($senderEmail);
$mail->addTo($receiverEmail);
$mail->setSubject($subject);
$mail->setBodyHtml($text);
$content = file_get_contents($attachmentAbsolutePath);
$attachment = new \Zend_Mime_Part($content);
$attachment->type = 'application/pdf';
$attachment->disposition = \Zend_Mime::DISPOSITION_ATTACHMENT;
$attachment->encoding = \Zend_Mime::ENCODING_BASE64;
$attachment->filename = $filename;
$mail->addAttachment($attachment);
$mail->send();
Frequently Asked Questions
Can I send multiple attachments in one email?
Yes. If you use the TransportBuilder extension method, ensure your prepareMessage loop iterates through an array of parts. In the Helper approach, simply loop through your file array and call addPart() for each file before calling sendMessage().
What Mime Type should I use for PDF files?
You should use application/pdf. For generic binary files, application/octet-stream is often used as a fallback.
Why are my email attachments not showing up in Magento 2.4?
In Magento 2.4, the system uses Laminas. Ensure you are not using old Zend_ classes if your environment has fully migrated. Check that your di.xml preference is correctly pointing to your custom TransportBuilder and that you have cleared the generated/code directory.
Wrapping Up
Adding attachments to emails in Magento 2 requires a bit of manual effort, but it is a vital skill for any Adobe Commerce developer. For long-term maintainability, extending the TransportBuilder is the cleanest approach for site-wide availability. However, if you are building a standalone module, the Helper approach provides better isolation.
Always remember to test your implementation across different email clients (like Gmail and Outlook) to ensure that MIME parts are being decoded correctly and that your attachments are accessible to your customers.