1<?php 2 3/** 4 * @see https://github.com/laminas/laminas-mail for the canonical source repository 5 * @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md 6 * @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License 7 */ 8 9namespace Laminas\Mail\Header; 10 11use Laminas\Mail\Address; 12use Laminas\Mail\AddressList; 13use Laminas\Mail\Headers; 14use TrueBV\Exception\OutOfBoundsException; 15use TrueBV\Punycode; 16 17/** 18 * Base class for headers composing address lists (to, from, cc, bcc, reply-to) 19 */ 20abstract class AbstractAddressList implements HeaderInterface 21{ 22 /** 23 * @var AddressList 24 */ 25 protected $addressList; 26 27 /** 28 * @var string Normalized field name 29 */ 30 protected $fieldName; 31 32 /** 33 * Header encoding 34 * 35 * @var string 36 */ 37 protected $encoding = 'ASCII'; 38 39 /** 40 * @var string lower case field name 41 */ 42 protected static $type; 43 44 /** 45 * @var Punycode|null 46 */ 47 private static $punycode; 48 49 public static function fromString($headerLine) 50 { 51 list($fieldName, $fieldValue) = GenericHeader::splitHeaderLine($headerLine); 52 if (strtolower($fieldName) !== static::$type) { 53 throw new Exception\InvalidArgumentException(sprintf( 54 'Invalid header line for "%s" string', 55 __CLASS__ 56 )); 57 } 58 59 // split value on "," 60 $fieldValue = str_replace(Headers::FOLDING, ' ', $fieldValue); 61 $fieldValue = preg_replace('/[^:]+:([^;]*);/', '$1,', $fieldValue); 62 $values = ListParser::parse($fieldValue); 63 64 $wasEncoded = false; 65 $addresses = array_map( 66 function ($value) use (&$wasEncoded) { 67 $decodedValue = HeaderWrap::mimeDecodeValue($value); 68 $wasEncoded = $wasEncoded || ($decodedValue !== $value); 69 70 $value = trim($decodedValue); 71 72 $comments = self::getComments($value); 73 $value = self::stripComments($value); 74 75 $value = preg_replace( 76 [ 77 '#(?<!\\\)"(.*)(?<!\\\)"#', // quoted-text 78 '#\\\([\x01-\x09\x0b\x0c\x0e-\x7f])#', // quoted-pair 79 ], 80 [ 81 '\\1', 82 '\\1', 83 ], 84 $value 85 ); 86 87 return empty($value) ? null : Address::fromString($value, $comments); 88 }, 89 $values 90 ); 91 $addresses = array_filter($addresses); 92 93 $header = new static(); 94 if ($wasEncoded) { 95 $header->setEncoding('UTF-8'); 96 } 97 98 /** @var AddressList $addressList */ 99 $addressList = $header->getAddressList(); 100 foreach ($addresses as $address) { 101 $addressList->add($address); 102 } 103 104 return $header; 105 } 106 107 public function getFieldName() 108 { 109 return $this->fieldName; 110 } 111 112 /** 113 * Safely convert UTF-8 encoded domain name to ASCII 114 * @param string $domainName the UTF-8 encoded email 115 * @return string 116 */ 117 protected function idnToAscii($domainName) 118 { 119 if (null === self::$punycode) { 120 self::$punycode = new Punycode(); 121 } 122 try { 123 return self::$punycode->encode($domainName); 124 } catch (OutOfBoundsException $e) { 125 return $domainName; 126 } 127 } 128 129 public function getFieldValue($format = HeaderInterface::FORMAT_RAW) 130 { 131 $emails = []; 132 $encoding = $this->getEncoding(); 133 134 foreach ($this->getAddressList() as $address) { 135 $email = $address->getEmail(); 136 $name = $address->getName(); 137 138 // quote $name if value requires so 139 if (! empty($name) && (false !== strpos($name, ',') || false !== strpos($name, ';'))) { 140 // FIXME: what if name contains double quote? 141 $name = sprintf('"%s"', $name); 142 } 143 144 if ($format === HeaderInterface::FORMAT_ENCODED 145 && 'ASCII' !== $encoding 146 ) { 147 if (! empty($name)) { 148 $name = HeaderWrap::mimeEncodeValue($name, $encoding); 149 } 150 151 if (preg_match('/^(.+)@([^@]+)$/', $email, $matches)) { 152 $localPart = $matches[1]; 153 $hostname = $this->idnToAscii($matches[2]); 154 $email = sprintf('%s@%s', $localPart, $hostname); 155 } 156 } 157 158 if (empty($name)) { 159 $emails[] = $email; 160 } else { 161 $emails[] = sprintf('%s <%s>', $name, $email); 162 } 163 } 164 165 // Ensure the values are valid before sending them. 166 if ($format !== HeaderInterface::FORMAT_RAW) { 167 foreach ($emails as $email) { 168 HeaderValue::assertValid($email); 169 } 170 } 171 172 return implode(',' . Headers::FOLDING, $emails); 173 } 174 175 public function setEncoding($encoding) 176 { 177 $this->encoding = $encoding; 178 return $this; 179 } 180 181 public function getEncoding() 182 { 183 return $this->encoding; 184 } 185 186 /** 187 * Set address list for this header 188 * 189 * @param AddressList $addressList 190 */ 191 public function setAddressList(AddressList $addressList) 192 { 193 $this->addressList = $addressList; 194 } 195 196 /** 197 * Get address list managed by this header 198 * 199 * @return AddressList 200 */ 201 public function getAddressList() 202 { 203 if (null === $this->addressList) { 204 $this->setAddressList(new AddressList()); 205 } 206 return $this->addressList; 207 } 208 209 public function toString() 210 { 211 $name = $this->getFieldName(); 212 $value = $this->getFieldValue(HeaderInterface::FORMAT_ENCODED); 213 return (empty($value)) ? '' : sprintf('%s: %s', $name, $value); 214 } 215 216 /** 217 * Retrieve comments from value, if any. 218 * 219 * Supposed to be private, protected as a workaround for PHP bug 68194 220 * 221 * @param string $value 222 * @return string 223 */ 224 protected static function getComments($value) 225 { 226 $matches = []; 227 preg_match_all( 228 '/\\( 229 (?P<comment>( 230 \\\\.| 231 [^\\\\)] 232 )+) 233 \\)/x', 234 $value, 235 $matches 236 ); 237 return isset($matches['comment']) ? implode(', ', $matches['comment']) : ''; 238 } 239 240 /** 241 * Strip all comments from value, if any. 242 * 243 * Supposed to be private, protected as a workaround for PHP bug 68194 244 * 245 * @param string $value 246 * @return string 247 */ 248 protected static function stripComments($value) 249 { 250 return preg_replace( 251 '/\\( 252 ( 253 \\\\.| 254 [^\\\\)] 255 )+ 256 \\)/x', 257 '', 258 $value 259 ); 260 } 261} 262