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
EmailTask fails with
Error: Call to undefined methodSummary
Queue\Queue\Task\EmailTaskcan fail withCall to undefined methodwhen rebuilding an email from queued JSON data, especially when the payload contains settings generated from a serializedCake\Mailer\Message.Some keys in
settingsmap toCake\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" } }EmailTaskeventually runs:Those calls fail because
Cake\Mailer\Message::setAppCharset()andCake\Mailer\Message::setHtmlMessage()are not implemented.Error:
Expected behavior
EmailTaskshould call the right methods, matching CakePHP's mailer API:Why this became visible
Older queue payloads could preserve richer PHP values through PHP serialization. After moving
queued_jobs.datato JSON, the worker receives plain arrays and needs to rebuild the email fromsettings.Reproduction
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.With this implementation the generic loop with
call_user_func_arraycalls is no longer necessary.Workaround
Override
Cake\Mailer\Mailerand implement the missing methods, then configure it asQueue.mailerClass.Related issue: #471