Skip to content

Unknown named parameter in EmailTask #471

@GustavoPeixoto

Description

@GustavoPeixoto

EmailTask fails with Unknown named parameter when address settings are associative arrays

Summary

Queue\Queue\Task\EmailTask can fail with Unknown named parameter when rebuilding an email from queued JSON data and the email address settings are stored as associative arrays, for example:

[
    'to' => [
        'recipient@example.com' => 'Recipient Name',
    ],
    'from' => [
        'sender@example.com' => 'Sender Name',
    ],
]

Those address maps are valid CakePHP mailer inputs, but EmailTask expands them through call_user_func_array(). On PHP 8+, string keys in the argument array are treated as named parameters, so the email address becomes a parameter name.

Current behavior

Given this queued payload:

{
  "settings": {
    "to": {
      "recipient@example.com": "Recipient Name"
    },
    "from": {
      "sender@example.com": "Sender Name"
    }
  },
  "content": "Test message"
}

EmailTask eventually runs:

$setter = 'setTo';
$setting = [
    'recipient@example.com' => 'Recipient Name',
];

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

On PHP 8+, this is interpreted similarly to:

$this->mailer->setTo(recipient@example.com: 'Recipient Name');

That fails because setTo() does not have a parameter named recipient@example.com.

Error:

Unknown named parameter $recipient@example.com

The same issue can happen with from, and potentially with other address settings that accept an associative email => name map.

Expected behavior

EmailTask should accept associative address arrays as a single positional argument, matching CakePHP's mailer API:

$this->mailer->setTo([
    'recipient@example.com' => 'Recipient Name',
]);

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.

When those settings contain associative address maps, the current call_user_func_array() usage expands the map incorrectly under PHP 8 named-argument semantics.

Reproduction

$data = [
    'settings' => [
        'to' => [
            'recipient@example.com' => 'Recipient Name',
        ],
        'from' => [
            'sender@example.com' => 'Sender Name',
        ],
        'subject' => 'Test',
    ],
    'content' => 'Test message',
];

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

Expected: the task sends or prepares the email.

Actual: the task throws Unknown named parameter.

Possible fix

Normalize the argument list passed to call_user_func_array().

For address setters such as to, from, cc, bcc, and replyTo, if the setting is an associative array, wrap it as a single positional argument instead of expanding the address map itself:

if (in_array($method, ['to', 'from', 'cc', 'bcc', 'replyTo'], true) && is_array($setting)) {
    $isAssociative = array_keys($setting) !== range(0, count($setting) - 1);

    if ($isAssociative) {
        call_user_func_array([$this->mailer, $setter], [$setting]);
        continue;
    }
}

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

This keeps the existing tuple-style payload working:

'to' => ['recipient@example.com', 'Recipient Name']

while also supporting CakePHP's associative address map format:

'to' => [
    'recipient@example.com' => 'Recipient Name',
]

Alternative possible fix

Handle address settings explicitly before the generic settings loop.

Address setters such as setTo(), setFrom(), setCc(), setBcc(), and setReplyTo() already accept an associative email => name array as their first argument. These settings can be consumed before the generic call_user_func_array() path:

$addressArguments = static function (mixed $setting): array {
    if (
        is_array($setting)
        && array_is_list($setting)
        && count($setting) === 2
        && is_string($setting[0])
        && ($setting[1] === null || is_string($setting[1]))
    ) {
        return $setting;
    }

    return [$setting];
};

if (array_key_exists('to', $settings)) {
    $this->mailer->setTo(...$addressArguments($settings['to']));
    unset($settings['to']);
}

if (array_key_exists('from', $settings)) {
    $this->mailer->setFrom(...$addressArguments($settings['from']));
    unset($settings['from']);
}

if (array_key_exists('cc', $settings)) {
    $this->mailer->setCc(...$addressArguments($settings['cc']));
    unset($settings['cc']);
}

if (array_key_exists('bcc', $settings)) {
    $this->mailer->setBcc(...$addressArguments($settings['bcc']));
    unset($settings['bcc']);
}

if (array_key_exists('replyTo', $settings)) {
    $this->mailer->setReplyTo(...$addressArguments($settings['replyTo']));
    unset($settings['replyTo']);
}

if (array_key_exists('theme', $settings)) {
    $this->mailer->viewBuilder()->setTheme($settings['theme']);
    unset($settings['theme']);
}

if (array_key_exists('template', $settings)) {
    $this->mailer->viewBuilder()->setTemplate($settings['template']);
    unset($settings['template']);
}

if (array_key_exists('layout', $settings)) {
    $this->mailer->viewBuilder()->setLayout($settings['layout']);
    unset($settings['layout']);
}

if (array_key_exists('helper', $settings)) {
    $this->mailer->viewBuilder()->addHelper(...(array)$settings['helper']);
    unset($settings['helper']);
}

if (array_key_exists('helpers', $settings)) {
    $this->mailer->viewBuilder()->addHelpers($settings['helpers']);
    unset($settings['helpers']);
}

if (array_key_exists('attachments', $settings)) {
    $this->mailer->setAttachments($settings['attachments']);
    unset($settings['attachments']);
}

foreach ($settings as $method => $setting) {
    $setter = 'set' . ucfirst($method);
    call_user_func_array([$this->mailer, $setter], (array)$setting);
}

This keeps the generic loop only for ordinary mailer settings and makes the special cases explicit before the fallback path.

A regression test should cover to and from using the associative email => name format.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions