How to send email with a file attachment.


M2 does not come with out of the box however it is a feature built into the zend framework. Here is a good reference how to add this functionality into magento: https://blog.bitexpert.de/blog/sending-mails-with-attachments-in-magento-2/

In case link goes dead, create the following

namespace Your\CustomModule\Magento\Mail\Template;

class TransportBuilder 
    extends \Magento\Framework\Mail\Template\TransportBuilder
    public function addAttachment(
        $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;

then add to etc/di.xml

<?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" />

Now you can use addAttachment() throughout your site.

| improve this answer | |
  • 10
    I still wonder why magento TransportBuilder does not have this method – Murtuza Zabuawala Jun 28 '17 at 8:15
  • 5
    How can we attached file in custom email magento 2.3 ? because its using zendframework 2 and this answer not working any more – Manish Maheshwari Dec 7 '18 at 5:51
  • 3
    How to Send Email with Attachment in Magento 2.3? – Dhaduk Mitesh Dec 31 '18 at 5:35
  • @ManishMaheshwari & Mitesh Have you got the solution? – Sameer Bhayani Jan 8 '19 at 10:14
  • 2
    This solution is not working in Magento2.3 anymore. Is anyone have alternative for attachment.? – nishu Aug 6 '19 at 13:24

As of Magento 2.2.7 the solutions described above do not work anymore since \Magento\Framework\Mail\Message dropped extending \Zend_Mail.
To circumvent the lack of an easy way to add attachments through the transport builder (which currently seems to be the correct place for such a function), one needs to create a replacement for the TransportBuilder and make use \Zend\Mime\Part:

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
    ) {

        $this->mimePartFactory    = $mimePartFactory;
        $this->mimeMessageFactory = $mimeMessageFactory;

    protected function prepareMessage()

        $mimeMessage = $this->getMimeMessage($this->message);

        foreach ($this->parts as $part) {


        return $this;

    public function addAttachment(
        $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(
        $type = Mime::TYPE_OCTETSTREAM,
        $disposition = Mime::DISPOSITION_ATTACHMENT,
        $encoding = Mime::ENCODING_BASE64,
        $filename = null
    ) {
        /** @var MimePart $mimePart */
        $mimePart = $this->mimePartFactory->create(['content' => $content]);

        if ($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);

        return $mimeMessage;

Don't forget to replace the original \Magento\Framework\Mail\Template\TransportBuilder by your implementation via di.xml.

Note that this implementation will probably break with an upcoming release of Magento as \Magento\Framework\Mail\MessageInterface::setBody() is deprecated and may be removed soonish.


| improve this answer | |
  • Hi! You have a method addAttachment in your code, but where did you call them? I don't see it. – Nikolai Silin May 21 '19 at 15:36
  • thanks! I added loop to prepareMessage method and everithing works. – Nikolai Silin May 21 '19 at 16:03
  • @NikolaiSilin how to send a png or other files. – sumeet bajaj Jun 5 '19 at 12:18
  • @Jean-Bernard Valentaten it worked in Magento 2.2.11, thank you so much this saves a lot of time – Mohit Rane Sep 2 at 12:08

Magento 2.3.x Compatible:

This was my answer for Magento 2.3 since this was a top question on google and there seems to be a lot of people in the comments looking.

There seems to be a lot of desire in other posts about overwriting the default TransportBuilder class via etc/di.xml, however the module I'm working on is so small that I don't want it to be responsible for the default TransportBuilder so I built a Helper class (should probably be a model based on how coupled it is to the declared email template - but I digress).

The TransportBuilder doesn't have public access to the TransportInterface, but instead generates a clone everytime and then resets the builder. I found it easier to build my TransportInterface instance and then attach my attachment Part objects to the transport's message. If you do find it necessary to overwrite the default TransportBuilder via dependency injection preference, be careful about updating public methods. Remember to practice the O when keeping your code SOLID!

namespace 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;

 * This was initially built out to send a single email. Abstract this as you 
 * wish.
 * @package Vendor\Module\Helper
class Mail extends AbstractHelper
     * @var Context
    protected $context;

     * @var TransportBuilder
    protected $transportBuilder;

     * @var StoreManagerInterface
    protected $storeManager;

     * @var Config
    protected $config;

     * Mail constructor.
     * @param Context $context
     * @param TransportBuilder $transportBuilder
     * @param StoreManagerInterface $storeManager
     * @param Config $config
     * @param File $file
    public function __construct(
        Context $context,
        TransportBuilder $transportBuilder,
        StoreManagerInterface $storeManager,
        Config $config,
        File $file
    ) {
        $this->transportBuilder = $transportBuilder;
        $this->storeManager = $storeManager;
        $this->config = $config;
        $this->file = $file;

     * Send the email for a Help Center submission.
     * @param DataObject $templateParams
     * @param array $attachments
     * @return void
    public function send(DataObject $templateParams, array $attachments = [])
        $storeId = $this->storeManager->getStore()->getId();

        // Build transport
        /** @var \Magento\Framework\Mail\TransportInterface $transport */
        $transport = $this->transportBuilder
            ->setTemplateOptions(['area' => Area::AREA_FRONTEND, 'store' => $storeId])
            ->addTo($this->config->getEmailRecipient(), 'Help Center')
             * Something important to note is that when the getTransport()
             * function is run, the message is compiled and then the builder 
             * class resets (as of 2.3.1). 
             * This is note worthy because if you want to send > 1 attachment,
             * your $builder will be reset -- losing all of the ->set* functions
             * you just used above as well as your attachment.
             * Since we append attachments to the transport, it's easier to:
             * build -> attach -> send. And this way multiple attachments 
             * can be included. :thumbsup:

        // Attach Images to transport
        foreach ($attachments as $a) {
            $transport = $this->addAttachment($transport, $a);

        // Send transport

     * Add an attachment to the message inside the transport builder.
     * @param TransportInterface $transportBuilder
     * @param array $file Sanitized index from $_FILES
     * @return TransportInterface
    protected function addAttachment(TransportInterface $transport, array $file): TransportInterface
        $part = $this->createAttachment($file);

        return $transport;

     * Create an zend mime part that is an attachment to attach to the email.
     * This was my usecase, you'll need to edit this to your own needs.
     * @param array $file Sanitized index from $_FILES
     * @return Part
    protected function createAttachment(array $file): Part
        $ext =  '.' . explode('/', $file['type'])[1];
        $fileName = md5(uniqid(microtime()), true) . $ext;

        $attachment = new Part($this->file->read($file['tmp_name']));
        $attachment->disposition = Zend_Mime::TYPE_OCTETSTREAM;
        $attachment->encoding = Zend_Mime::ENCODING_BASE64;
        $attachment->filename = $fileName;

        return $attachment;
| improve this answer | |
  • 1
    I can't get it to work properly, I always get an exception saying "Uncaught Error: Call to a member function addPart() on string"... any idea about that? :/ – hallleron Nov 6 '19 at 9:30
  • 1
    @hallleron Oddly enough this is different than what I was getting, but it looks like you're correct. The MessageInterface::getBody method signature shows a string return type. You might have to dig around in your TransportInterface object, but I can tell you that the addPart method exists on a Zend\Mime\Message object. Since magento likely extended that class for their own Message class, I think it would be smart to try $transport->getMessage()->addpart($part); – domdambrogia Nov 6 '19 at 17:01
  • How to apply this in Magento 2.3? – fudu Jul 29 at 4:34

Magento 2 Custom email from Module, Doesn't provide image attachment.

If you want to use Image attachment with email templates in Magento 2 you need to override class, Magento\Framework\Mail\Template\TransportBuilder

Magento Out-of-box doesn't provide attachment feature for email. You can refer blogs for send image attachment in details,

You need to add logic like below way,

 public function addAttachment(
        $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;
| improve this answer | |
  • 1
    Can you help to achieve same in magento 2.3 ? – Sameer Bhayani Jan 8 '19 at 10:15
  • Created attachments in this way until 2.2.7. 2.2.8 and 2.3+ are not working – Matthias Kleine Mar 30 '19 at 17:05
  • I just posted an answer for 2.3.x @MatthiasKleine – domdambrogia Sep 20 '19 at 21:18
  • hello,how can i attach if i have base64 encoding string? – Ketan Borada Sep 24 '19 at 13:09

As mentioned by the previous answers, magento2 does not have a function out-of-the-box to send mails with attachments.

I don't know if it is a best practice, but you could call directly Zend_Mail class to do it, without create a custom function and override Magento\Framework\Mail\Template\TransportBuilder, like below

$mail = new \Zend_Mail('utf-8');

$content = file_get_contents($attachmentAbsolutePath);

$attachment = new \Zend_Mime_Part($content);
$attachment->type = 'text/xml'; // attachment's mime type
$attachment->disposition = \Zend_Mime::DISPOSITION_ATTACHMENT;
$attachment->encoding = \Zend_Mime::ENCODING_BASE64;
$attachment->filename = $filename;
| improve this answer | |
  • before give -1, it is suggested by so to use this comment textarea, then everyone could understand what is wrong, thx – LucScu Apr 2 '19 at 7:57
  • $transport->getMessage()->setBody($bodyPart); – imtiazau Nov 22 '19 at 5:54
  • getting this Uncaught Error: Call to undefined method Magento\\Framework\\Mail\\EmailMessage::setBody() – imtiazau Nov 22 '19 at 5:55
  • These comments are not related to the answer – LucScu Nov 22 '19 at 7:20
  • i am getting this error in magento 2.3.3 – imtiazau Nov 22 '19 at 7:59

Here is perfect answer to send pdf in Email in magetno 2.3

$transport = $_transportBuilder->setTemplateIdentifier(20)

$html = $transport->getMessage()>getBody()->generateMessage();            
$bodyMessage = new \Zend\Mime\Part($html);
$bodyMessage->type = 'text/html';
$attachment = $_transportBuilder->addAttachment($pdfData,$fileName);      
$bodyPart = new \Zend\Mime\Message();
| improve this answer | |
  • Hi, It is throwing a fatal error: Uncaught Error: Call to a member function generateMessage() on null – gajjala sandeep Sep 11 '19 at 5:42
  • You're creating a new message which is unnecessary when your transport already has a message. Why not just add a part to the one in place? This is messy and hard to follow. Not to mention you're doubling the work and memory needed to solve this problem. – domdambrogia Sep 20 '19 at 21:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.