Skip to content

Unknown methods in EmailTask #473

@GustavoPeixoto

Description

@GustavoPeixoto

EmailTask fails with Error: Call to undefined method

Summary

Queue\Queue\Task\EmailTask can fail with Call to undefined method when rebuilding an email from queued JSON data, especially when the payload contains settings generated from a serialized Cake\Mailer\Message.

Some keys in settings map to Cake\Mailer\Message::$serializableProperties, but not all of them have matching setter methods.

Current behavior

Given this queued payload:

{
  "transport": "default",
  "settings": {
    "to": {
      "recipient@example.com": "Recipient Name"
    },
    "from": {
      "sender@example.com": "Sender Name"
    },
    "domain": "domain.com",
    "charset": "utf-8",
    "subject": "Message Subject",
    "messageId": true,
    "emailFormat": "html",
    "emailPattern": "/^((?:[\\p{L}0-9.!#$%&'*+\\/=?^_`{|}~-]+)*@[\\p{L}0-9-._]+)$/ui",
    "appCharset": "UTF-8",
    "headerCharset": "utf-8",
    "htmlMessage": "Message body"
  }
}

EmailTask eventually runs:

$setter = 'setAppCharset';
$setting = 'UTF-8';

call_user_func_array([$this->mailer, $setter], (array)$setting);
$setter = 'setHtmlMessage';
$setting = 'Message body';

call_user_func_array([$this->mailer, $setter], (array)$setting);

Those calls fail because Cake\Mailer\Message::setAppCharset() and Cake\Mailer\Message::setHtmlMessage() are not implemented.

Error:

Error: Call to undefined method Cake\Mailer\Message::setAppCharset() in /www/vendor/cakephp/cakephp/src/Mailer/Mailer.php:286

Expected behavior

EmailTask should call the right methods, matching CakePHP's mailer API:

$this->message->setBodyHtml('Message body');

Why this became visible

Older queue payloads could preserve richer PHP values through PHP serialization. After moving queued_jobs.data to JSON, the worker receives plain arrays and needs to rebuild the email from settings.

Reproduction

$data = [
    "transport" => "default",
    "settings" => [
        "to" => [
            "recipient@example.com" => "Recipient Name",
        ],
        "from" => [
            "sender@example.com" => "Sender Name",
        ],
        "domain" => "domain.com",
        "charset" => "utf-8",
        "subject" => "Message Subject",
        "messageId" => true,
        "emailFormat" => "html",
        "emailPattern" => "/^((?:[\\p{L}0-9.!#$%&'*+\\/=?^_`{|}~-]+)*@[\\p{L}0-9-._]+)$/ui",
        "appCharset" => "UTF-8",
        "headerCharset" => "utf-8",
        "htmlMessage" => "Message body",
    ],
];

$task->run($data, 1);

Expected: the task sends or prepares the email.

Actual: the task throws Call to undefined method.

Possible fix

Stop using call_user_func_array() for special cases and make the method calls explicit, so method references are easier to observe and maintain.

<?php
$settings = $data['settings'] + $this->defaults;

$appCharset = Configure::read('App.encoding');

try {
    if (array_key_exists('appCharset', $settings)) {
        Configure::write('App.encoding', $settings['appCharset']);
        unset($settings['appCharset']);
    }

    $this->mailer = $this->getMailer();
} finally {
    Configure::write('App.encoding', $appCharset);
}

$message = $this->mailer->getMessage();
$viewBuilder = $this->mailer->viewBuilder();

$map = [
    'to' => $message->setTo(...),
    'from' => $message->setFrom(...),
    'replyTo' => $message->setReplyTo(...),
    'cc' => $message->setCc(...),
    'bcc' => $message->setBcc(...),
    'sender' => $message->setSender(...),
    'returnPath' => $message->setReturnPath(...),
    'readReceipt' => $message->setReadReceipt(...),
];
foreach ($map as $settingKey => $settingMethod) {
    if (array_key_exists($settingKey, $settings)) {
        $settingMethod(...$this->addressArguments($settings[$settingKey]));
        unset($settings[$settingKey]);
    }
}

$map = [
    'subject' => $message->setSubject(...),
    'emailFormat' => $message->setEmailFormat(...),
    'emailPattern' => $message->setEmailPattern(...),
    'domain' => $message->setDomain(...),
    'messageId' => $message->setMessageId(...),
    'charset' => $message->setCharset(...),
    'headerCharset' => $message->setHeaderCharset(...),
    'textMessage' => $message->setBodyText(...),
    'htmlMessage' => $message->setBodyHtml(...),

    'theme' => $viewBuilder->setTheme(...),
    'template' => $viewBuilder->setTemplate(...),
    'layout' => $viewBuilder->setLayout(...),
    'helper' => $viewBuilder->addHelper(...),
];

foreach ($map as $settingKey => $settingMethod) {
    if (array_key_exists($settingKey, $settings)) {
        $settingMethod(...(array)$settings[$settingKey]);
        unset($settings[$settingKey]);
    }
}

$map = [
    'attachments' => $message->setAttachments(...),
    'headers' => $message->setHeaders(...),
    'helpers' => $viewBuilder->addHelpers(...),
];

foreach ($map as $settingKey => $settingMethod) {
    if (array_key_exists($settingKey, $settings)) {
        $settingMethod($settings[$settingKey]);
        unset($settings[$settingKey]);
    }
}

With this implementation the generic loop with call_user_func_array calls is no longer necessary.

Workaround

Override Cake\Mailer\Mailer and implement the missing methods, then configure it as Queue.mailerClass.

<?php

declare(strict_types=1);

namespace App\Mailer;

final class Mailer extends \Cake\Mailer\Mailer
{
    public function setAppCharset(string $charset): Mailer
    {
        if (is_null($this->getMessage()->getCharset())) {
            $this->getMessage()->setCharset($charset);
        }

        return $this;
    }

    public function setTextMessage(string $content): Mailer
    {
        $this->getMessage()->setBodyText($content);
        return $this;
    }

    public function setHtmlMessage(string $content): Mailer
    {
        $this->getMessage()->setBodyHtml($content);
        return $this;
    }
}

Related issue: #471

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions