Skip to content
This repository has been archived by the owner on Jan 30, 2020. It is now read-only.

unable to parse To headers with semicolon separator #147

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ All notable changes to this project will be documented in this file, in reverse
- [#213](https://github.com/zendframework/zend-mail/pull/213) re-adds support for PHP 5.6 and 7.0; ZF policy is never
to bump the major version of a PHP requirement unless the package is bumping major version.

### Changed

- Nothing.

### Deprecated

- Nothing.
Expand All @@ -19,7 +23,8 @@ All notable changes to this project will be documented in this file, in reverse

### Fixed

- Nothing.
- [#147](https://github.com/zendframework/zend-mail/pull/147) fixes how address lists are parsed, expanding the functionality to allow either
`,` or `;` delimiters (or both in combination).

## 2.9.0 - 2017-03-01

Expand Down
13 changes: 9 additions & 4 deletions src/AddressList.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ public function add($emailOrAddress, $name = null)
{
if (is_string($emailOrAddress)) {
$emailOrAddress = $this->createAddress($emailOrAddress, $name);
} elseif (! $emailOrAddress instanceof Address\AddressInterface) {
}

if (! $emailOrAddress instanceof Address\AddressInterface) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects an email address or %s\Address object as its first argument; received "%s"',
__METHOD__,
Expand Down Expand Up @@ -65,14 +67,17 @@ public function addMany(array $addresses)
foreach ($addresses as $key => $value) {
if (is_int($key) || is_numeric($key)) {
$this->add($value);
} elseif (is_string($key)) {
$this->add($key, $value);
} else {
continue;
}

if (! is_string($key)) {
throw new Exception\RuntimeException(sprintf(
'Invalid key type in provided addresses array ("%s")',
(is_object($key) ? get_class($key) : var_export($key, 1))
));
}

$this->add($key, $value);
}
return $this;
}
Expand Down
3 changes: 2 additions & 1 deletion src/Header/AbstractAddressList.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public static function fromString($headerLine)
// split value on ","
$fieldValue = str_replace(Headers::FOLDING, ' ', $fieldValue);
$fieldValue = preg_replace('/[^:]+:([^;]*);/', '$1,', $fieldValue);
$values = str_getcsv($fieldValue, ',');
$values = AddressListParser::parse($fieldValue);

$wasEncoded = false;
array_walk(
Expand Down Expand Up @@ -80,6 +80,7 @@ function (&$value) use (&$wasEncoded) {

$values = array_filter($values);

/** @var AddressList $addressList */
$addressList = $header->getAddressList();
foreach ($values as $address) {
$addressList->addFromString($address);
Expand Down
86 changes: 86 additions & 0 deletions src/Header/AddressListParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php
/**
* @see https://github.com/zendframework/zend-mail for the canonical source repository
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-mail/blob/master/LICENSE.md New BSD License
*/

namespace Zend\Mail\Header;

use function in_array;

class AddressListParser
{
const CHAR_QUOTES = ['\'', '"'];
const CHAR_DELIMS = [',', ';'];
const CHAR_ESCAPE = '\\';

/**
* @param string $value
* @return array
*/
public static function parse($value)
{
$values = [];
$length = strlen($value);
$currentValue = '';
$inEscape = false;
$inQuote = false;
$currentQuoteDelim = null;

for ($i = 0; $i < $length; $i += 1) {
$char = $value[$i];

// If we are in an escape sequence, append the character and continue.
if ($inEscape) {
$currentValue .= $char;
$inEscape = false;
continue;
}

// If we are not in a quoted string, and have a delimiter, append
// the current value to the list, and reset the current value.
if (in_array($char, self::CHAR_DELIMS, true) && ! $inQuote) {
$values [] = $currentValue;
$currentValue = '';
continue;
}

// Append the character to the current value
$currentValue .= $char;

// Escape sequence discovered.
if (self::CHAR_ESCAPE === $char) {
$inEscape = true;
continue;
}

// If the character is not a quote character, we are done
// processing it.
if (! in_array($char, self::CHAR_QUOTES)) {
continue;
}

// If the character matches a previously matched quote delimiter,
// we reset our quote status and the currently opened quote
// delimiter.
if ($char === $currentQuoteDelim) {
$inQuote = false;
$currentQuoteDelim = null;
continue;
}

// Otherwise, we're starting a quoted string.
$inQuote = true;
$currentQuoteDelim = $char;
}

// If we reached the end of the string and still have a current value,
// append it to the list (no delimiter was reached).
if ('' !== $currentValue) {
$values [] = $currentValue;
}

return $values;
}
}
27 changes: 27 additions & 0 deletions test/AddressListTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use PHPUnit\Framework\TestCase;
use Zend\Mail\Address;
use Zend\Mail\AddressList;
use Zend\Mail\Exception\InvalidArgumentException;
use Zend\Mail\Header;

/**
* @group Zend_Mail
Expand Down Expand Up @@ -105,4 +107,29 @@ public function testDoesNotStoreDuplicatesAndFirstWins()
$address = $this->list->get('zf-devteam@zend.com');
$this->assertNull($address->getName());
}

/**
* Microsoft Outlook sends emails with semicolon separated To addresses.
*
* @see https://blogs.msdn.microsoft.com/oldnewthing/20150119-00/?p=44883
*/
public function testSemicolonSeparator()
{
$header = 'Some User <some.user@example.com>; uzer2.surname@example.org;'
. ' asda.fasd@example.net, root@example.org';

// In previous versions, this throws: 'The input exceeds the allowed
// length'; hence the try/catch block, to allow finding the root cause.
try {
$to = Header\To::fromString('To:' . $header);
} catch (InvalidArgumentException $e) {
$this->fail('Header\To::fromString should not throw');
}
$addressList = $to->getAddressList();

$this->assertEquals('Some User', $addressList->get('some.user@example.com')->getName());
$this->assertTrue($addressList->has('uzer2.surname@example.org'));
$this->assertTrue($addressList->has('asda.fasd@example.net'));
$this->assertTrue($addressList->has('root@example.org'));
}
}