PHP SwiftMailer StringReplacementFilter bug workaround

149 Views Asked by At

Trying to implement a temp workaround to deal with this weird SwiftMailer bug:

https://github.com/swiftmailer/swiftmailer/issues/762

When reading a file that has a length of exactly n*8192 bytes (n >= 1), the last > value of $bytes is an empty string which triggers the error.

@raffomania's comment on GitHub:

we found that the following adaption of AbstractFilterableInputStream.write would fix the problem for us:

public function write($bytes)
{
    $this->_writeBuffer .= $bytes;
    if (empty($this->_writeBuffer)) {
        return;
    }
    foreach ($this->_filters as $filter) {
        if ($filter->shouldBuffer($this->_writeBuffer)) {
            return;
        }
    }
    $this->_doWrite($this->_writeBuffer);

    return ++$this->_sequence;
}

I'd like to extend the AbstractFilterableInputStream class and call this modified write method when AbstractFilterableInputStream is called by SwiftMailer.

I'm using the Laravel framework.

1

There are 1 best solutions below

2
On BEST ANSWER

The best way to solve this is to have the swiftmailer forked and fixed and you use your own forked version of swiftmailer. However if you do not want to do this, this fix is rather lengthy but it should work. Give it a shot. If there is any issue do let me know.

1. Create app/Mail/CustomFileByteStream.php: This is to have your own version of write.

<?php

namespace App\Mail;

/**
 * Allows reading and writing of bytes to and from a file.
 *
 * @author Chris Corbyn
 */
class CustomFileByteStream extends \Swift_ByteStream_FileByteStream
{
    public function write($bytes)
    {
        $this->_writeBuffer .= $bytes;
        if (empty($this->_writeBuffer)) {
            return;
        }
        foreach ($this->_filters as $filter) {
            if ($filter->shouldBuffer($this->_writeBuffer)) {
                return;
            }
        }
        $this->_doWrite($this->_writeBuffer);

        return ++$this->_sequence;
    }
}

2. Create app/Mail/CustomSwiftAttachment.php: So that it uses the custom FileByteStream

<?php

namespace App\Mail;

/**
 * Attachment class for attaching files to a {@link Swift_Mime_Message}.
 *
 * @author Chris Corbyn
 */
class CustomSwiftAttachment extends \Swift_Attachment
{
    /**
     * Create a new Attachment from a filesystem path.
     *
     * @param string $path
     * @param string $contentType optional
     *
     * @return Swift_Mime_Attachment
     */
    public static function fromPath($path, $contentType = null)
    {
        return self::newInstance()->setFile(
            new CustomFileByteStream($path),
            $contentType
            );
    }
}

3. Create app/Mail/CustomSwiftImage.php: So that it uses the custom FileByteStream

<?php

namespace App\Mail;

/**
 * An image, embedded in a multipart message.
 *
 * @author Chris Corbyn
 */
class CustomSwiftImage extends \Swift_Image
{
    /**
     * Create a new Image from a filesystem path.
     *
     * @param string $path
     *
     * @return Swift_Image
     */
    public static function fromPath($path)
    {
        $image = self::newInstance()->setFile(
            new CustomFileByteStream($path)
            );

        return $image;
    }
}

4. Create app/Mail/Message.php: So that it uses your own custom Swift_Image and Swift_Attachment

<?php

namespace App\Mail;

use Illuminate\Mail\Message as DefaultMessage;

class Message extends DefaultMessage
{
    /**
     * Create a Swift Attachment instance.
     *
     * @param  string  $file
     * @return CustomSwiftAttachment
     */
    protected function createAttachmentFromPath($file)
    {
        return CustomSwiftAttachment::fromPath($file);
    }

    /**
     * Embed a file in the message and get the CID.
     *
     * @param  string  $file
     * @return string
     */
    public function embed($file)
    {
        return $this->swift->embed(CustomSwiftImage::fromPath($file));
    }
}

5. Create app/Mail/Mailer.php: So it uses your custom Message class

<?php

namespace App\Mail;

use Swift_Message;
use Illuminate\Mail\Mailer as DefaultMailer;

class Mailer extends DefaultMailer
{
    /**
     * Create a new message instance. Notice this is complete replacement of parent's version.
     * We uses our own "Message" class instead of theirs.
     *
     * @return \Illuminate\Mail\Message
     */
    protected function createMessage()
    {
        $message = new Message(new Swift_Message);

        // If a global from address has been specified we will set it on every message
        // instances so the developer does not have to repeat themselves every time
        // they create a new message. We will just go ahead and push the address.
        if (! empty($this->from['address'])) {
            $message->from($this->from['address'], $this->from['name']);
        }

        return $message;
    }
}

6. Create app/Mail/MailServiceProvider.php: So it uses your custom Mailer class

<?php

namespace App\Mail;

use Illuminate\Mail\MailServiceProvider as DefaultMailServiceProvider;
use App\Mail\Mailer;

/**
 * This mail service provider is almost identical with the illuminate version, with the exception that
 * we are hijacking with our own Message class
 */
class MailServiceProvider extends DefaultMailServiceProvider
{
    /**
     * Complete replacement of parent register class so we can
     * overwrite the use of mailer class. Notice the "Mailer" class is points to our own
     * version of mailer, so we can hijack the message class.
     *
     * @return void
     */
    public function register()
    {
        $this->registerSwiftMailer();

        $this->app->singleton('mailer', function ($app) {
            // Once we have create the mailer instance, we will set a container instance
            // on the mailer. This allows us to resolve mailer classes via containers
            // for maximum testability on said classes instead of passing Closures.
            $mailer = new Mailer(
                $app['view'], $app['swift.mailer'], $app['events']
            );

            $this->setMailerDependencies($mailer, $app);

            // If a "from" address is set, we will set it on the mailer so that all mail
            // messages sent by the applications will utilize the same "from" address
            // on each one, which makes the developer's life a lot more convenient.
            $from = $app['config']['mail.from'];

            if (is_array($from) && isset($from['address'])) {
                $mailer->alwaysFrom($from['address'], $from['name']);
            }

            $to = $app['config']['mail.to'];

            if (is_array($to) && isset($to['address'])) {
                $mailer->alwaysTo($to['address'], $to['name']);
            }

            return $mailer;
        });
    }
}

7. Edit config/app.php

  • Comment out Illuminate\Mail\MailServiceProvider::class,
  • Add this below of the line above App\Mail\MailServiceProvider::class,

So this will use your custom MailServiceProvider. At any point of time if you wish to revert this, just delete all the files above and reverse this edit.


Calling sequence:

enter image description here

So here you go. This should hijack the MailServiceProvider to use your own custom Swift_ByteStream_FileByteStream. Hopefully no typos!