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

Commit 8aac5f7

Browse files
committed
Merge branch 'hotfix/147-to-split-semicolon'
Close #147
2 parents 91f3972 + 83f9dde commit 8aac5f7

File tree

5 files changed

+130
-6
lines changed

5 files changed

+130
-6
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ All notable changes to this project will be documented in this file, in reverse
99
- [#213](https://github.com/zendframework/zend-mail/pull/213) re-adds support for PHP 5.6 and 7.0; ZF policy is never
1010
to bump the major version of a PHP requirement unless the package is bumping major version.
1111

12+
### Changed
13+
14+
- Nothing.
15+
1216
### Deprecated
1317

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

2024
### Fixed
2125

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

2429
## 2.9.0 - 2017-03-01
2530

src/AddressList.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ public function add($emailOrAddress, $name = null)
3131
{
3232
if (is_string($emailOrAddress)) {
3333
$emailOrAddress = $this->createAddress($emailOrAddress, $name);
34-
} elseif (! $emailOrAddress instanceof Address\AddressInterface) {
34+
}
35+
36+
if (! $emailOrAddress instanceof Address\AddressInterface) {
3537
throw new Exception\InvalidArgumentException(sprintf(
3638
'%s expects an email address or %s\Address object as its first argument; received "%s"',
3739
__METHOD__,
@@ -65,14 +67,17 @@ public function addMany(array $addresses)
6567
foreach ($addresses as $key => $value) {
6668
if (is_int($key) || is_numeric($key)) {
6769
$this->add($value);
68-
} elseif (is_string($key)) {
69-
$this->add($key, $value);
70-
} else {
70+
continue;
71+
}
72+
73+
if (! is_string($key)) {
7174
throw new Exception\RuntimeException(sprintf(
7275
'Invalid key type in provided addresses array ("%s")',
7376
(is_object($key) ? get_class($key) : var_export($key, 1))
7477
));
7578
}
79+
80+
$this->add($key, $value);
7681
}
7782
return $this;
7883
}

src/Header/AbstractAddressList.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public static function fromString($headerLine)
5050
// split value on ","
5151
$fieldValue = str_replace(Headers::FOLDING, ' ', $fieldValue);
5252
$fieldValue = preg_replace('/[^:]+:([^;]*);/', '$1,', $fieldValue);
53-
$values = str_getcsv($fieldValue, ',');
53+
$values = AddressListParser::parse($fieldValue);
5454

5555
$wasEncoded = false;
5656
array_walk(
@@ -80,6 +80,7 @@ function (&$value) use (&$wasEncoded) {
8080

8181
$values = array_filter($values);
8282

83+
/** @var AddressList $addressList */
8384
$addressList = $header->getAddressList();
8485
foreach ($values as $address) {
8586
$addressList->addFromString($address);

src/Header/AddressListParser.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
/**
3+
* @see https://github.com/zendframework/zend-mail for the canonical source repository
4+
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com)
5+
* @license https://github.com/zendframework/zend-mail/blob/master/LICENSE.md New BSD License
6+
*/
7+
8+
namespace Zend\Mail\Header;
9+
10+
use function in_array;
11+
12+
class AddressListParser
13+
{
14+
const CHAR_QUOTES = ['\'', '"'];
15+
const CHAR_DELIMS = [',', ';'];
16+
const CHAR_ESCAPE = '\\';
17+
18+
/**
19+
* @param string $value
20+
* @return array
21+
*/
22+
public static function parse($value)
23+
{
24+
$values = [];
25+
$length = strlen($value);
26+
$currentValue = '';
27+
$inEscape = false;
28+
$inQuote = false;
29+
$currentQuoteDelim = null;
30+
31+
for ($i = 0; $i < $length; $i += 1) {
32+
$char = $value[$i];
33+
34+
// If we are in an escape sequence, append the character and continue.
35+
if ($inEscape) {
36+
$currentValue .= $char;
37+
$inEscape = false;
38+
continue;
39+
}
40+
41+
// If we are not in a quoted string, and have a delimiter, append
42+
// the current value to the list, and reset the current value.
43+
if (in_array($char, self::CHAR_DELIMS, true) && ! $inQuote) {
44+
$values [] = $currentValue;
45+
$currentValue = '';
46+
continue;
47+
}
48+
49+
// Append the character to the current value
50+
$currentValue .= $char;
51+
52+
// Escape sequence discovered.
53+
if (self::CHAR_ESCAPE === $char) {
54+
$inEscape = true;
55+
continue;
56+
}
57+
58+
// If the character is not a quote character, we are done
59+
// processing it.
60+
if (! in_array($char, self::CHAR_QUOTES)) {
61+
continue;
62+
}
63+
64+
// If the character matches a previously matched quote delimiter,
65+
// we reset our quote status and the currently opened quote
66+
// delimiter.
67+
if ($char === $currentQuoteDelim) {
68+
$inQuote = false;
69+
$currentQuoteDelim = null;
70+
continue;
71+
}
72+
73+
// Otherwise, we're starting a quoted string.
74+
$inQuote = true;
75+
$currentQuoteDelim = $char;
76+
}
77+
78+
// If we reached the end of the string and still have a current value,
79+
// append it to the list (no delimiter was reached).
80+
if ('' !== $currentValue) {
81+
$values [] = $currentValue;
82+
}
83+
84+
return $values;
85+
}
86+
}

test/AddressListTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
use PHPUnit\Framework\TestCase;
1111
use Zend\Mail\Address;
1212
use Zend\Mail\AddressList;
13+
use Zend\Mail\Exception\InvalidArgumentException;
14+
use Zend\Mail\Header;
1315

1416
/**
1517
* @group Zend_Mail
@@ -105,4 +107,29 @@ public function testDoesNotStoreDuplicatesAndFirstWins()
105107
$address = $this->list->get('[email protected]');
106108
$this->assertNull($address->getName());
107109
}
110+
111+
/**
112+
* Microsoft Outlook sends emails with semicolon separated To addresses.
113+
*
114+
* @see https://blogs.msdn.microsoft.com/oldnewthing/20150119-00/?p=44883
115+
*/
116+
public function testSemicolonSeparator()
117+
{
118+
$header = 'Some User <[email protected]>; [email protected];'
119+
120+
121+
// In previous versions, this throws: 'The input exceeds the allowed
122+
// length'; hence the try/catch block, to allow finding the root cause.
123+
try {
124+
$to = Header\To::fromString('To:' . $header);
125+
} catch (InvalidArgumentException $e) {
126+
$this->fail('Header\To::fromString should not throw');
127+
}
128+
$addressList = $to->getAddressList();
129+
130+
$this->assertEquals('Some User', $addressList->get('[email protected]')->getName());
131+
$this->assertTrue($addressList->has('[email protected]'));
132+
$this->assertTrue($addressList->has('[email protected]'));
133+
$this->assertTrue($addressList->has('[email protected]'));
134+
}
108135
}

0 commit comments

Comments
 (0)