Search code examples
magentomagento2laminas

Magento 2 Laminas mail 'Invalid header value' error when email name contains German characters


When I try to send mail from admin order, if customer name has special German/Danish characters, email is not sending. Sending perfectly for other customers. The error I found was Invalid header value

I traced the error to file vendor/laminas/laminas-http/src/Header/HeaderValue.php

/**
     * Assert a header value is valid.
     *
     * @param string $value
     * @throws Exception\RuntimeException For invalid values.
     * @return void
     */
    public static function assertValid($value)
    {
        if (! self::isValid($value)) {
            throw new Exception\InvalidArgumentException('Invalid header value');
        }
    }

This function was getting called from vendor/laminas/laminas-mail/src/Header/AbstractAddressList.php

public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
    {
        $emails   = [];
        $encoding = $this->getEncoding();

        foreach ($this->getAddressList() as $address) {
            $email = $address->getEmail();
            $name  = $address->getName();

            // quote $name if value requires so
            if (! empty($name) && (false !== strpos($name, ',') || false !== strpos($name, ';'))) {
                // FIXME: what if name contains double quote?
                $name = sprintf('"%s"', $name);
            }

            if ($format === HeaderInterface::FORMAT_ENCODED
                && 'ASCII' !== $encoding
            ) {
                if (! empty($name)) {
                    $name = HeaderWrap::mimeEncodeValue($name, $encoding);
                }

                if (preg_match('/^(.+)@([^@]+)$/', $email, $matches)) {
                    $localPart = $matches[1];
                    $hostname  = $this->idnToAscii($matches[2]);
                    $email = sprintf('%s@%s', $localPart, $hostname);
                }
            }

            if (empty($name)) {
                $emails[] = $email;
            } else {
                $emails[] = sprintf('%s <%s>', $name, $email);
            }
        }

        // Ensure the values are valid before sending them.
        if ($format !== HeaderInterface::FORMAT_RAW) {
            foreach ($emails as $email) {
                HeaderValue::assertValid($email);
            }
        }

        return implode(',' . Headers::FOLDING, $emails);
    }

I have found a class Magento\Framework\Filter\RemoveAccents which replaces special characters with their usable counterparts, but as the function assertValid is a static function inside a final class, and function getFieldValue is inside an abstract class, I am unable to replace the characters.


Solution

  • I got the solution by following Tyler's instruction to go back up the stack trace. Let me explain.

    Class Laminas\Mail\Header\AbstractAddressList::getFieldValue uses the following code to get customer name and email

    foreach ($this->getAddressList() as $address) {
            $email = $address->getEmail();
            $name  = $address->getName();
    

    Here $this->getAddressList() comes from

    /**
     * Get address list managed by this header
     *
     * @return AddressList
     */
    public function getAddressList()
    {
        if (null === $this->addressList) {
            $this->setAddressList(new AddressList());
        }
        return $this->addressList;
    }
    

    Where $this->addressList gets assigned on

    /**
     * Set address list for this header
     *
     * @param  AddressList $addressList
     */
    public function setAddressList(AddressList $addressList)
    {
        $this->addressList = $addressList;
    }
    

    It uses Laminas\Mail\AddressList class. So going in there

    /**
     * Create an address object
     *
     * @param  string $email
     * @param  string|null $name
     * @return Address
     */
    protected function createAddress($email, $name)
    {
        return new Address($email, $name);
    }
    

    is used to set name and email. Here it creates object of class Laminas\Mail\Address. So going in there, I found

    /**
     * Retrieve name, if any
     *
     * @return null|string
     */
    public function getName()
    {
        return $this->name;
    }
    

    Modifying this return value was fixing my problem. So I created a plugin.

    public function afterGetName(\Laminas\Mail\Address $subject,$result)
    {
        $removeAccent = new \Magento\Framework\Filter\RemoveAccents(true);
        return $removeAccent->filter($result);
    }