My Cart

Add Email Attachments in Magento 2

A step-by-step guide to attaching PDF invoices and other files to Magento 2 sales emails.

Introduction

Adding attachments like PDF invoices, shipments, or custom files to Magento 2 sales emails can enhance customer experience. This guide walks you through implementing email attachments using a custom module, with custom transport builder code examples from the Vendor\Module\Model\Email\TransportBuilder class.

In Magento 2.4.8 (and newer), the laminas-mime library has been removed. To send email attachments now, you should use Symfony’s Mime via Magento’s native Mail framework. Here's a concise guide combining best practices and our refined implementation.

Steps to Add Email Attachments

  • Create di.xml

    Create a di.xml file in the Vendor/Module/etc/ directory.

    <?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="Vendor\Module\Model\Email\TransportBuilder" />
    </config>
        
  • Define Custom Email TransportBuilder

    Create or customize a TransportBuilder class to support email attachments in Magento 2.4.8+, replacing the legacy Laminas MIME system. Below is a basic structure of the class with a sample getTransport() method.

    <?php
    namespace Vendor\Module\Model\Email;
    
    use Magento\Framework\HTTP\Mime;
    use Magento\Framework\Mail\Template\FactoryInterface;
    use Magento\Framework\Mail\Template\SenderResolverInterface;
    use Magento\Framework\Mail\Template\TransportBuilder as CoreTransportBuilder;
    use Magento\Framework\App\TemplateTypesInterface;
    use Magento\Framework\Exception\LocalizedException;
    use Magento\Framework\Mail\AddressConverter;
    use Magento\Framework\Mail\Exception\InvalidArgumentException;
    use Magento\Framework\Exception\MailException;
    use Magento\Framework\Mail\MessageInterface;
    use Magento\Framework\Mail\MimeInterface;
    use Magento\Framework\Mail\EmailMessageInterfaceFactory;
    use Magento\Framework\Mail\MimeMessageInterfaceFactory;
    use Magento\Framework\Mail\MimePartInterfaceFactory;
    use Magento\Framework\Mail\TransportInterfaceFactory;
    use Magento\Framework\ObjectManagerInterface;
    use Symfony\Component\Mime\Part\DataPart;
    use Symfony\Component\Mime\Part\Multipart\MixedPart;
    
    class TransportBuilder extends CoreTransportBuilder
    {
        /** @var array */
        private $messageData = [];
    
        /**
         * @var EmailMessageInterfaceFactory
         */
        private $emailMessageInterfaceFactory;
    
        /**
         * @var MimeMessageInterfaceFactory
         */
        private $mimeMessageInterfaceFactory;
    
        /**
         * @var MimePartInterfaceFactory
         */
        private $mimePartInterfaceFactory;
    
        /**
         * @var AddressConverter
         */
        private $addressConverter;
    
        /**
         * @var string
         */
        private $messageBodyParts = [];
    
        /**
         * @param FactoryInterface $templateFactory
         * @param MessageInterface $message
         * @param SenderResolverInterface $senderResolver
         * @param ObjectManagerInterface $objectManager
         * @param TransportInterfaceFactory $mailTransportFactory
         * @param EmailMessageInterfaceFactory $emailMessageInterfaceFactory
         * @param MimeMessageInterfaceFactory $mimeMessageInterfaceFactory
         * @param MimePartInterfaceFactory $mimePartInterfaceFactory
         * @param AddressConverter $addressConverter
         */
        public function __construct(
            FactoryInterface $templateFactory,
            MessageInterface $message,
            SenderResolverInterface $senderResolver,
            ObjectManagerInterface $objectManager,
            TransportInterfaceFactory $mailTransportFactory,
            EmailMessageInterfaceFactory $emailMessageInterfaceFactory,
            MimeMessageInterfaceFactory $mimeMessageInterfaceFactory,
            MimePartInterfaceFactory $mimePartInterfaceFactory,
            AddressConverter $addressConverter
        ) {
            parent::__construct(
                $templateFactory,
                $message,
                $senderResolver,
                $objectManager,
                $mailTransportFactory
            );
            $this->emailMessageInterfaceFactory = $emailMessageInterfaceFactory;
            $this->mimeMessageInterfaceFactory = $mimeMessageInterfaceFactory;
            $this->mimePartInterfaceFactory = $mimePartInterfaceFactory;
            $this->addressConverter = $addressConverter;
        }
    
        /**
         * Add cc address
         *
         * @param array|string $address
         * @param string $name
         *
         * @return $this
         */
        public function addCc($address, $name = '')
        {
            $this->addAddressByType('cc', $address, $name);
    
            return $this;
        }
    
        /**
         * Add to address
         *
         * @param array|string $address
         * @param string $name
         *
         * @return $this
         * @throws InvalidArgumentException
         */
        public function addTo($address, $name = '')
        {
            $this->addAddressByType('to', $address, $name);
    
            return $this;
        }
    
        /**
         * Add bcc address
         *
         * @param array|string $address
         *
         * @return $this
         * @throws InvalidArgumentException
         */
        public function addBcc($address)
        {
            $this->addAddressByType('bcc', $address);
    
            return $this;
        }
    
        /**
         * Set Reply-To Header
         *
         * @param string $email
         * @param string|null $name
         *
         * @return $this
         * @throws InvalidArgumentException
         */
        public function setReplyTo($email, $name = null)
        {
            $this->addAddressByType('replyTo', $email, $name);
    
            return $this;
        }
    
        /**
         * Set mail from address
         *
         * @param string|array $from
         *
         * @return $this
         * @throws InvalidArgumentException
         * @see setFromByScope()
         *
         * @deprecated 102.0.1 This function sets the from address but does not provide
         * a way of setting the correct from addresses based on the scope.
         */
        public function setFrom($from)
        {
            return $this->setFromByScope($from);
        }
    
        /**
         * Set mail from address by scopeId
         *
         * @param string|array $from
         * @param string|int $scopeId
         *
         * @return $this
         * @throws InvalidArgumentException
         * @throws MailException
         * @since 102.0.1
         */
        public function setFromByScope($from, $scopeId = null)
        {
            $result = $this->_senderResolver->resolve($from, $scopeId);
            $this->addAddressByType('from', $result['email'], $result['name']);
    
            return $this;
        }
    
        /**
         * Reset object state
         *
         * @return $this
         */
        protected function reset()
        {
            $this->messageData = [];
            $this->templateIdentifier = null;
            $this->templateVars = null;
            $this->templateOptions = null;
            $this->messageBodyParts = [];
    
            return $this;
        }
    
        /**
         * Prepare message.
         *
         * @return $this
         * @throws LocalizedException if template type is unknown
         */
        protected function prepareMessage()
        {
            $template = $this->getTemplate();
            $content = $template->processTemplate();
    
            switch ($template->getType()) {
                case TemplateTypesInterface::TYPE_TEXT:
                    $partType = MimeInterface::TYPE_TEXT;
                    break;
    
                case TemplateTypesInterface::TYPE_HTML:
                    $partType = MimeInterface::TYPE_HTML;
                    break;
    
                default:
                    throw new LocalizedException(
                        new Phrase('Unknown template type')
                    );
            }
    
            /** @var \Magento\Framework\Mail\MimePartInterface $mimePart */
            $mimePart = $this->mimePartInterfaceFactory->create(
                [
                    'content' => $content,
                    'type' => $partType
                ]
            );
            $this->messageData['encoding'] = $mimePart->getCharset();
            $this->messageData['body'] = $this->mimeMessageInterfaceFactory->create(
                ['parts' => [$mimePart]]
            );
    
            $this->messageData['subject'] = html_entity_decode(
                (string)$template->getSubject(),
                ENT_QUOTES
            );
    
            $this->message = $this->emailMessageInterfaceFactory->create($this->messageData);
            if (count($this->messageBodyParts)) {
                $this->messageData['body']->getMimeMessage()->setBody(
                    new MixedPart($this->messageData['body']->getMimeMessage()->getBody(), ...$this->messageBodyParts)
                );
            }
    
            return $this;
        }
    
        /**
         * Handles possible incoming types of email (string or array)
         *
         * @param string $addressType
         * @param string|array $email
         * @param string|null $name
         *
         * @return void
         * @throws InvalidArgumentException
         */
        private function addAddressByType(string $addressType, $email, ?string $name = null): void
        {
            if (is_string($email)) {
                $this->messageData[$addressType][] = $this->addressConverter->convert($email, $name);
                return;
            }
            $convertedAddressArray = $this->addressConverter->convertMany($email);
            if (isset($this->messageData[$addressType])) {
                $this->messageData[$addressType] = array_merge(
                    $this->messageData[$addressType],
                    $convertedAddressArray
                );
            } else {
                $this->messageData[$addressType] = $convertedAddressArray;
            }
        }
    
        /**
         * @inheritdoc
         */
        public function addAttachment(
            $body,
            $filename = null,
            $mimeType = Mime::TYPE_OCTETSTREAM
        ) {
            $this->messageBodyParts[] = new DataPart(
                $body,
                $filename,
                $mimeType,
                Mime::ENCODING_BASE64
            );
    
            return $this;
        }
    }
                            
  • Configure and Send Email with Custom TransportBuilder

    Use the custom TransportBuilder (defined in di.xml) to send emails with attachments. Inject the custom class into your service and configure it with template variables and attachments. Below is an example using the sendEmail method:

    customTransportBuilder // Use custom TransportBuilder
        ->setTemplateIdentifier($this->scopeConfig->getValue(self::EMAIL_SEND_TO_SUPPLIER_TEMPLATE))
        ->setTemplateOptions(
            [
                'area' => \Magento\Framework\App\Area::AREA_FRONTEND,
                'store' => \Magento\Store\Model\Store::DEFAULT_STORE_ID,
            ]
        )
        ->setTemplateVars([
            'purchase_order' => $purchaseOrder,
            'supplier' => $supplier,
            'logo_url' => $logo_src
        ])
        ->setFrom($sender)
        ->addTo(trim($supplier->getContactEmail()))
        ->addAttachment($fileContent, 'PurchaseOrder-' . $poCode . '.pdf');
            ?>

    Note: Ensure the custom TransportBuilder class (Vendor\Module\Model\Email\TransportBuilder) extends Magento\Framework\Mail\Template\TransportBuilder and implements the addAttachment method to handle file attachments correctly. Avoid circular dependencies by not injecting the service back into the custom builder.

  • Configure Email Template

    Create an email template in the Magento admin panel under Content > Email Templates. Assign it a unique templateId for use in the sendEmail method.

  • Test the Implementation

    Test the email functionality by triggering an order email or using a custom script to call the sendEmail method. Verify that attachments (e.g., PDF invoices) are included and that the email renders correctly across clients like Gmail and Outlook. Use web-safe fonts (e.g., Arial) and keep attachment sizes below 100KB to avoid issues like Gmail clipping. Additionally, configure the MagePlaza SMTP module for reliable email delivery: navigate to Stores > Configuration > MagePlaza Extensions > SMTP Configuration in the Magento admin panel, enable the SMTP extension, and set up your email provider credentials (e.g., Gmail SMTP server: smtp.gmail.com, port 587, with TLS). Test the connection and save the configuration to ensure emails are sent through your preferred SMTP server.

Related Products