Webklex / laravel-imap

Laravel IMAP is an easy way to integrate both the native php-imap module and an extended custom imap protocol into your Laravel app.
https://www.php-imap.com
MIT License
637 stars 181 forks source link

Serialization Message object with attachments #475

Open vmrfriz opened 1 year ago

vmrfriz commented 1 year ago

Is your feature request related to a problem? Please describe. Using laravel-imap, I receive emails from mail and pass them to handlers in laravel queues. When passed to the queue, the object is serialized to JSON in the Illuminate\Queue\Queue::create Payload() method. As a result, I get an error:

Illuminate\Queue\InvalidPayloadException  Unable to JSON encode payload. Error code: 5.

--
 () at vendor/laravel/framework/src/Illuminate/Queue/Queue.php:108
 Illuminate\Queue\Queue->createPayload() at vendor/laravel/framework/src/Illuminate/Queue/DatabaseQueue.php:92
 Illuminate\Queue\DatabaseQueue->push() at vendor/laravel/framework/src/Illuminate/Queue/Queue.php:57
 Illuminate\Queue\Queue->pushOn() at vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php:591
 Illuminate\Events\Dispatcher->queueHandler() at vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php:515
 Illuminate\Events\Dispatcher->Illuminate\Events\{closure}() at vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php:441
 Illuminate\Events\Dispatcher->Illuminate\Events\{closure}() at vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php:249
 Illuminate\Events\Dispatcher->dispatch() at vendor/laravel/framework/src/Illuminate/Foundation/helpers.php:433
 event() at vendor/laravel/framework/src/Illuminate/Foundation/Events/Dispatchable.php:14
 App\Events\EmailReceived::dispatch() at eval()'d code:2
 eval() at vendor/psy/psysh/src/ExecutionLoopClosure.php:53
 Psy\{closure}() at vendor/psy/psysh/src/ExecutionClosure.php:89
 Psy\ExecutionClosure->execute() at vendor/psy/psysh/src/Shell.php:395
 Psy\Shell->doInteractiveRun() at vendor/psy/psysh/src/Shell.php:366
 Psy\Shell->doRun() at vendor/symfony/console/Application.php:168
 Symfony\Component\Console\Application->run() at vendor/psy/psysh/src/Shell.php:341
 Psy\Shell->run() at vendor/laravel/tinker/src/Console/TinkerCommand.php:85
 Laravel\Tinker\Console\TinkerCommand->handle() at vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:36
 Illuminate\Container\BoundMethod::Illuminate\Container\{closure}() at vendor/laravel/framework/src/Illuminate/Container/Util.php:41
 Illuminate\Container\Util::unwrapIfClosure() at vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:93
 Illuminate\Container\BoundMethod::callBoundMethod() at vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:35
 Illuminate\Container\BoundMethod::call() at vendor/laravel/framework/src/Illuminate/Container/Container.php:662
 Illuminate\Container\Container->call() at vendor/laravel/framework/src/Illuminate/Console/Command.php:208
 Illuminate\Console\Command->execute() at vendor/symfony/console/Command/Command.php:312
 Symfony\Component\Console\Command\Command->run() at vendor/laravel/framework/src/Illuminate/Console/Command.php:177
 Illuminate\Console\Command->run() at vendor/symfony/console/Application.php:1040
 Symfony\Component\Console\Application->doRunCommand() at vendor/symfony/console/Application.php:314
 Symfony\Component\Console\Application->doRun() at vendor/symfony/console/Application.php:168
 Symfony\Component\Console\Application->run() at vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php:200
 Illuminate\Foundation\Console\Kernel->handle() at artisan:35

Describe the solution you'd like For the solution, I added the methods __serialize() and __unserialize to the event object. After studying the source code of the \Webklex\PHPIMAP\Message class, I decided to do it by analogy with the save() and fromString() methods. It turned out the following:

<?php

namespace App\Events;

use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Webklex\PHPIMAP\ClientManager;
use Webklex\PHPIMAP\Message;

class EmailReceived
{
    use Dispatchable;
    use InteractsWithSockets;
    use SerializesModels;

    /**
     * Create a new event instance.
     */
    public function __construct(
        public Message $message
    ) {
        //
    }

    public function __serialize()
    {
        return [
            'message' => $this->message->getHeader()->raw . "\r\n\r\n" . $this->message->getRawBody(),
        ];
    }

    public function __unserialize(array $values)
    {
        // Load config for \Webklex\PHPIMAP\Message class
        new ClientManager();

        $this->message = Message::fromString($values['message']);
    }
}

I think the same solution can be applied for the \Webklex\PHPIMAP\Message class

Describe alternatives you've considered Initially, I tried to solve the problem by serializing the \Webklex\PHPIMAP\Message object, but I wanted the event to accept an instance of the class, not a string.

In addition, serialize() and unserialize() incorrectly expand Attribute and adjacent objects. I found this fix in the issue to webklex\phpimap: #179.

Additional context Without initializing new Client Manager();, the method \Webklex\PHPIMAP\Message::fromString() does not work. Therefore, I had to initialize it in the __unserialize() method. Perhaps there is a better solution?

This is what I get if I don't initialize new \Webklex\PHPIMAP\ClientManager():

Cannot assign null to property Webklex\PHPIMAP\Message::$config of type array {"exception":"[object] (TypeError(code: 0): Cannot assign null to property Webklex\\PHPIMAP\\Message::$config of type array at /run/media/mrfriz/Windows/xampp/htdocs/carzaem.local/vendor/webklex/php-imap/src/Message.php:368)\n[previous exception] [object] (TypeError(code: 0): Cannot assign null to property Webklex\\PHPIMAP\\Message::$config of type array at /run/media/mrfriz/Windows/xampp/htdocs/carzaem.local/vendor/webklex/php-imap/src/Message.php:368)"}
Webklex commented 1 year ago

Hi @vmrfriz , many thanks for your suggestion. I like the idea, but I'm afraid of another big "thing" - how to handle the active client connection? Or have you tested it and it already restores itself somehow?

If you would like to tackle this and perhaps create a pull request, please feel welcome to do so over here: https://github.com/Webklex/php-imap

webklex/php-imap is the core library this laravel package utilizes.

Best regards and happy coding,