1<?php 2/** 3 * @see https://github.com/zendframework/zend-mail for the canonical source repository 4 * @copyright Copyright (c) 2005-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 8namespace Zend\Mail; 9 10use ArrayIterator; 11use Countable; 12use Iterator; 13use Traversable; 14use Zend\Loader\PluginClassLocator; 15use Zend\Mail\Header\GenericHeader; 16use Zend\Mail\Header\HeaderInterface; 17 18/** 19 * Basic mail headers collection functionality 20 * 21 * Handles aggregation of headers 22 */ 23class Headers implements Countable, Iterator 24{ 25 /** @var string End of Line for fields */ 26 const EOL = "\r\n"; 27 28 /** @var string Start of Line when folding */ 29 const FOLDING = "\r\n "; 30 31 /** 32 * @var \Zend\Loader\PluginClassLoader 33 */ 34 protected $pluginClassLoader = null; 35 36 /** 37 * @var array key names for $headers array 38 */ 39 protected $headersKeys = []; 40 41 /** 42 * @var Header\HeaderInterface[] instances 43 */ 44 protected $headers = []; 45 46 /** 47 * Header encoding; defaults to ASCII 48 * 49 * @var string 50 */ 51 protected $encoding = 'ASCII'; 52 53 /** 54 * Populates headers from string representation 55 * 56 * Parses a string for headers, and aggregates them, in order, in the 57 * current instance, primarily as strings until they are needed (they 58 * will be lazy loaded) 59 * 60 * @param string $string 61 * @param string $EOL EOL string; defaults to {@link EOL} 62 * @throws Exception\RuntimeException 63 * @return Headers 64 */ 65 public static function fromString($string, $EOL = self::EOL) 66 { 67 $headers = new static(); 68 $currentLine = ''; 69 $emptyLine = 0; 70 71 // iterate the header lines, some might be continuations 72 $lines = explode($EOL, $string); 73 $total = count($lines); 74 for ($i = 0; $i < $total; $i += 1) { 75 $line = $lines[$i]; 76 77 // Empty line indicates end of headers 78 // EXCEPT if there are more lines, in which case, there's a possible error condition 79 if (preg_match('/^\s*$/', $line)) { 80 $emptyLine += 1; 81 if ($emptyLine > 2) { 82 throw new Exception\RuntimeException('Malformed header detected'); 83 } 84 continue; 85 } 86 87 if ($emptyLine > 1) { 88 throw new Exception\RuntimeException('Malformed header detected'); 89 } 90 91 // check if a header name is present 92 if (preg_match('/^[\x21-\x39\x3B-\x7E]+:.*$/', $line)) { 93 if ($currentLine) { 94 // a header name was present, then store the current complete line 95 $headers->addHeaderLine($currentLine); 96 } 97 $currentLine = trim($line); 98 continue; 99 } 100 101 // continuation: append to current line 102 // recover the whitespace that break the line (unfolding, rfc2822#section-2.2.3) 103 if (preg_match('/^\s+.*$/', $line)) { 104 $currentLine .= ' ' . trim($line); 105 continue; 106 } 107 108 // Line does not match header format! 109 throw new Exception\RuntimeException(sprintf( 110 'Line "%s" does not match header format!', 111 $line 112 )); 113 } 114 if ($currentLine) { 115 $headers->addHeaderLine($currentLine); 116 } 117 return $headers; 118 } 119 120 /** 121 * Set an alternate implementation for the PluginClassLoader 122 * 123 * @param PluginClassLocator $pluginClassLoader 124 * @return Headers 125 */ 126 public function setPluginClassLoader(PluginClassLocator $pluginClassLoader) 127 { 128 $this->pluginClassLoader = $pluginClassLoader; 129 return $this; 130 } 131 132 /** 133 * Return an instance of a PluginClassLocator, lazyload and inject map if necessary 134 * 135 * @return PluginClassLocator 136 */ 137 public function getPluginClassLoader() 138 { 139 if ($this->pluginClassLoader === null) { 140 $this->pluginClassLoader = new Header\HeaderLoader(); 141 } 142 return $this->pluginClassLoader; 143 } 144 145 /** 146 * Set the header encoding 147 * 148 * @param string $encoding 149 * @return Headers 150 */ 151 public function setEncoding($encoding) 152 { 153 $this->encoding = $encoding; 154 foreach ($this as $header) { 155 $header->setEncoding($encoding); 156 } 157 return $this; 158 } 159 160 /** 161 * Get the header encoding 162 * 163 * @return string 164 */ 165 public function getEncoding() 166 { 167 return $this->encoding; 168 } 169 170 /** 171 * Add many headers at once 172 * 173 * Expects an array (or Traversable object) of type/value pairs. 174 * 175 * @param array|Traversable $headers 176 * @throws Exception\InvalidArgumentException 177 * @return Headers 178 */ 179 public function addHeaders($headers) 180 { 181 if (! is_array($headers) && ! $headers instanceof Traversable) { 182 throw new Exception\InvalidArgumentException(sprintf( 183 'Expected array or Traversable; received "%s"', 184 (is_object($headers) ? get_class($headers) : gettype($headers)) 185 )); 186 } 187 188 foreach ($headers as $name => $value) { 189 if (is_int($name)) { 190 if (is_string($value)) { 191 $this->addHeaderLine($value); 192 } elseif (is_array($value) && count($value) == 1) { 193 $this->addHeaderLine(key($value), current($value)); 194 } elseif (is_array($value) && count($value) == 2) { 195 $this->addHeaderLine($value[0], $value[1]); 196 } elseif ($value instanceof Header\HeaderInterface) { 197 $this->addHeader($value); 198 } 199 } elseif (is_string($name)) { 200 $this->addHeaderLine($name, $value); 201 } 202 } 203 204 return $this; 205 } 206 207 /** 208 * Add a raw header line, either in name => value, or as a single string 'name: value' 209 * 210 * This method allows for lazy-loading in that the parsing and instantiation of HeaderInterface object 211 * will be delayed until they are retrieved by either get() or current() 212 * 213 * @throws Exception\InvalidArgumentException 214 * @param string $headerFieldNameOrLine 215 * @param string $fieldValue optional 216 * @return Headers 217 */ 218 public function addHeaderLine($headerFieldNameOrLine, $fieldValue = null) 219 { 220 if (! is_string($headerFieldNameOrLine)) { 221 throw new Exception\InvalidArgumentException(sprintf( 222 '%s expects its first argument to be a string; received "%s"', 223 __METHOD__, 224 (is_object($headerFieldNameOrLine) 225 ? get_class($headerFieldNameOrLine) 226 : gettype($headerFieldNameOrLine)) 227 )); 228 } 229 230 if ($fieldValue === null) { 231 $headers = $this->loadHeader($headerFieldNameOrLine); 232 $headers = is_array($headers) ? $headers : [$headers]; 233 foreach ($headers as $header) { 234 $this->addHeader($header); 235 } 236 } elseif (is_array($fieldValue)) { 237 foreach ($fieldValue as $i) { 238 $this->addHeader(Header\GenericMultiHeader::fromString($headerFieldNameOrLine . ':' . $i)); 239 } 240 } else { 241 $this->addHeader(Header\GenericHeader::fromString($headerFieldNameOrLine . ':' . $fieldValue)); 242 } 243 244 return $this; 245 } 246 247 /** 248 * Add a Header\Interface to this container, for raw values see {@link addHeaderLine()} and {@link addHeaders()} 249 * 250 * @param Header\HeaderInterface $header 251 * @return Headers 252 */ 253 public function addHeader(Header\HeaderInterface $header) 254 { 255 $key = $this->normalizeFieldName($header->getFieldName()); 256 $this->headersKeys[] = $key; 257 $this->headers[] = $header; 258 if ($this->getEncoding() !== 'ASCII') { 259 $header->setEncoding($this->getEncoding()); 260 } 261 return $this; 262 } 263 264 /** 265 * Remove a Header from the container 266 * 267 * @param string|Header\HeaderInterface field name or specific header instance to remove 268 * @return bool 269 */ 270 public function removeHeader($instanceOrFieldName) 271 { 272 if ($instanceOrFieldName instanceof Header\HeaderInterface) { 273 $indexes = array_keys($this->headers, $instanceOrFieldName, true); 274 } else { 275 $key = $this->normalizeFieldName($instanceOrFieldName); 276 $indexes = array_keys($this->headersKeys, $key, true); 277 } 278 279 if (! empty($indexes)) { 280 foreach ($indexes as $index) { 281 unset($this->headersKeys[$index]); 282 unset($this->headers[$index]); 283 } 284 return true; 285 } 286 287 return false; 288 } 289 290 /** 291 * Clear all headers 292 * 293 * Removes all headers from queue 294 * 295 * @return Headers 296 */ 297 public function clearHeaders() 298 { 299 $this->headers = $this->headersKeys = []; 300 return $this; 301 } 302 303 /** 304 * Get all headers of a certain name/type 305 * 306 * @param string $name 307 * @return bool|ArrayIterator|Header\HeaderInterface Returns false if there is no headers with $name in this 308 * contain, an ArrayIterator if the header is a MultipleHeadersInterface instance and finally returns 309 * HeaderInterface for the rest of cases. 310 */ 311 public function get($name) 312 { 313 $key = $this->normalizeFieldName($name); 314 $results = []; 315 316 foreach (array_keys($this->headersKeys, $key) as $index) { 317 if ($this->headers[$index] instanceof Header\GenericHeader) { 318 $results[] = $this->lazyLoadHeader($index); 319 } else { 320 $results[] = $this->headers[$index]; 321 } 322 } 323 324 switch (count($results)) { 325 case 0: 326 return false; 327 case 1: 328 if ($results[0] instanceof Header\MultipleHeadersInterface) { 329 return new ArrayIterator($results); 330 } else { 331 return $results[0]; 332 } 333 //fall-trough 334 default: 335 return new ArrayIterator($results); 336 } 337 } 338 339 /** 340 * Test for existence of a type of header 341 * 342 * @param string $name 343 * @return bool 344 */ 345 public function has($name) 346 { 347 $name = $this->normalizeFieldName($name); 348 return in_array($name, $this->headersKeys); 349 } 350 351 /** 352 * Advance the pointer for this object as an iterator 353 * 354 */ 355 public function next() 356 { 357 next($this->headers); 358 } 359 360 /** 361 * Return the current key for this object as an iterator 362 * 363 * @return mixed 364 */ 365 public function key() 366 { 367 return key($this->headers); 368 } 369 370 /** 371 * Is this iterator still valid? 372 * 373 * @return bool 374 */ 375 public function valid() 376 { 377 return (current($this->headers) !== false); 378 } 379 380 /** 381 * Reset the internal pointer for this object as an iterator 382 * 383 */ 384 public function rewind() 385 { 386 reset($this->headers); 387 } 388 389 /** 390 * Return the current value for this iterator, lazy loading it if need be 391 * 392 * @return Header\HeaderInterface 393 */ 394 public function current() 395 { 396 $current = current($this->headers); 397 if ($current instanceof Header\GenericHeader) { 398 $current = $this->lazyLoadHeader(key($this->headers)); 399 } 400 return $current; 401 } 402 403 /** 404 * Return the number of headers in this contain, if all headers have not been parsed, actual count could 405 * increase if MultipleHeader objects exist in the Request/Response. If you need an exact count, iterate 406 * 407 * @return int count of currently known headers 408 */ 409 public function count() 410 { 411 return count($this->headers); 412 } 413 414 /** 415 * Render all headers at once 416 * 417 * This method handles the normal iteration of headers; it is up to the 418 * concrete classes to prepend with the appropriate status/request line. 419 * 420 * @return string 421 */ 422 public function toString() 423 { 424 $headers = ''; 425 foreach ($this as $header) { 426 if ($str = $header->toString()) { 427 $headers .= $str . self::EOL; 428 } 429 } 430 431 return $headers; 432 } 433 434 /** 435 * Return the headers container as an array 436 * 437 * @param bool $format Return the values in Mime::Encoded or in Raw format 438 * @return array 439 * @todo determine how to produce single line headers, if they are supported 440 */ 441 public function toArray($format = Header\HeaderInterface::FORMAT_RAW) 442 { 443 $headers = []; 444 /* @var $header Header\HeaderInterface */ 445 foreach ($this->headers as $header) { 446 if ($header instanceof Header\MultipleHeadersInterface) { 447 $name = $header->getFieldName(); 448 if (! isset($headers[$name])) { 449 $headers[$name] = []; 450 } 451 $headers[$name][] = $header->getFieldValue($format); 452 } else { 453 $headers[$header->getFieldName()] = $header->getFieldValue($format); 454 } 455 } 456 return $headers; 457 } 458 459 /** 460 * By calling this, it will force parsing and loading of all headers, after this count() will be accurate 461 * 462 * @return bool 463 */ 464 public function forceLoading() 465 { 466 foreach ($this as $item) { 467 // $item should now be loaded 468 } 469 return true; 470 } 471 472 /** 473 * Create Header object from header line 474 * 475 * @param string $headerLine 476 * @return Header\HeaderInterface|Header\HeaderInterface[] 477 */ 478 public function loadHeader($headerLine) 479 { 480 list($name, ) = Header\GenericHeader::splitHeaderLine($headerLine); 481 482 /** @var HeaderInterface $class */ 483 $class = $this->getPluginClassLoader()->load($name) ?: Header\GenericHeader::class; 484 return $class::fromString($headerLine); 485 } 486 487 /** 488 * @param $index 489 * @return mixed 490 */ 491 protected function lazyLoadHeader($index) 492 { 493 $current = $this->headers[$index]; 494 495 $key = $this->headersKeys[$index]; 496 497 /** @var GenericHeader $class */ 498 $class = ($this->getPluginClassLoader()->load($key)) ?: 'Zend\Mail\Header\GenericHeader'; 499 500 $encoding = $current->getEncoding(); 501 $headers = $class::fromString($current->toString()); 502 if (is_array($headers)) { 503 $current = array_shift($headers); 504 $current->setEncoding($encoding); 505 $this->headers[$index] = $current; 506 foreach ($headers as $header) { 507 $header->setEncoding($encoding); 508 $this->headersKeys[] = $key; 509 $this->headers[] = $header; 510 } 511 return $current; 512 } 513 514 $current = $headers; 515 $current->setEncoding($encoding); 516 $this->headers[$index] = $current; 517 return $current; 518 } 519 520 /** 521 * Normalize a field name 522 * 523 * @param string $fieldName 524 * @return string 525 */ 526 protected function normalizeFieldName($fieldName) 527 { 528 return str_replace(['-', '_', ' ', '.'], '', strtolower($fieldName)); 529 } 530} 531