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
14 use Zend\Loader\PluginClassLocator
;
15 use Zend\Mail\Header\GenericHeader
;
16 use Zend\Mail\Header\HeaderInterface
;
19 * Basic mail headers collection functionality
21 * Handles aggregation of headers
23 class Headers
implements Countable
, Iterator
25 /** @var string End of Line for fields */
28 /** @var string Start of Line when folding */
29 const FOLDING
= "\r\n ";
32 * @var \Zend\Loader\PluginClassLoader
34 protected $pluginClassLoader = null;
37 * @var array key names for $headers array
39 protected $headersKeys = [];
42 * @var Header\HeaderInterface[] instances
44 protected $headers = [];
47 * Header encoding; defaults to ASCII
51 protected $encoding = 'ASCII';
54 * Populates headers from string representation
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)
60 * @param string $string
61 * @param string $EOL EOL string; defaults to {@link EOL}
62 * @throws Exception\RuntimeException
65 public static function fromString($string, $EOL = self
::EOL
)
67 $headers = new static();
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) {
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)) {
82 throw new Exception\
RuntimeException('Malformed header detected');
88 throw new Exception\
RuntimeException('Malformed header detected');
91 // check if a header name is present
92 if (preg_match('/^[\x21-\x39\x3B-\x7E]+:.*$/', $line)) {
94 // a header name was present, then store the current complete line
95 $headers->addHeaderLine($currentLine);
97 $currentLine = trim($line);
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);
108 // Line does not match header format!
109 throw new Exception\
RuntimeException(sprintf(
110 'Line "%s" does not match header format!',
115 $headers->addHeaderLine($currentLine);
121 * Set an alternate implementation for the PluginClassLoader
123 * @param PluginClassLocator $pluginClassLoader
126 public function setPluginClassLoader(PluginClassLocator
$pluginClassLoader)
128 $this->pluginClassLoader
= $pluginClassLoader;
133 * Return an instance of a PluginClassLocator, lazyload and inject map if necessary
135 * @return PluginClassLocator
137 public function getPluginClassLoader()
139 if ($this->pluginClassLoader
=== null) {
140 $this->pluginClassLoader
= new Header\
HeaderLoader();
142 return $this->pluginClassLoader
;
146 * Set the header encoding
148 * @param string $encoding
151 public function setEncoding($encoding)
153 $this->encoding
= $encoding;
154 foreach ($this as $header) {
155 $header->setEncoding($encoding);
161 * Get the header encoding
165 public function getEncoding()
167 return $this->encoding
;
171 * Add many headers at once
173 * Expects an array (or Traversable object) of type/value pairs.
175 * @param array|Traversable $headers
176 * @throws Exception\InvalidArgumentException
179 public function addHeaders($headers)
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))
188 foreach ($headers as $name => $value) {
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);
199 } elseif (is_string($name)) {
200 $this->addHeaderLine($name, $value);
208 * Add a raw header line, either in name => value, or as a single string 'name: value'
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()
213 * @throws Exception\InvalidArgumentException
214 * @param string $headerFieldNameOrLine
215 * @param string $fieldValue optional
218 public function addHeaderLine($headerFieldNameOrLine, $fieldValue = null)
220 if (! is_string($headerFieldNameOrLine)) {
221 throw new Exception\
InvalidArgumentException(sprintf(
222 '%s expects its first argument to be a string; received "%s"',
224 (is_object($headerFieldNameOrLine)
225 ?
get_class($headerFieldNameOrLine)
226 : gettype($headerFieldNameOrLine))
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);
236 } elseif (is_array($fieldValue)) {
237 foreach ($fieldValue as $i) {
238 $this->addHeader(Header\GenericMultiHeader
::fromString($headerFieldNameOrLine . ':' . $i));
241 $this->addHeader(Header\GenericHeader
::fromString($headerFieldNameOrLine . ':' . $fieldValue));
248 * Add a Header\Interface to this container, for raw values see {@link addHeaderLine()} and {@link addHeaders()}
250 * @param Header\HeaderInterface $header
253 public function addHeader(Header\HeaderInterface
$header)
255 $key = $this->normalizeFieldName($header->getFieldName());
256 $this->headersKeys
[] = $key;
257 $this->headers
[] = $header;
258 if ($this->getEncoding() !== 'ASCII') {
259 $header->setEncoding($this->getEncoding());
265 * Remove a Header from the container
267 * @param string|Header\HeaderInterface field name or specific header instance to remove
270 public function removeHeader($instanceOrFieldName)
272 if ($instanceOrFieldName instanceof Header\HeaderInterface
) {
273 $indexes = array_keys($this->headers
, $instanceOrFieldName, true);
275 $key = $this->normalizeFieldName($instanceOrFieldName);
276 $indexes = array_keys($this->headersKeys
, $key, true);
279 if (! empty($indexes)) {
280 foreach ($indexes as $index) {
281 unset($this->headersKeys
[$index]);
282 unset($this->headers
[$index]);
293 * Removes all headers from queue
297 public function clearHeaders()
299 $this->headers
= $this->headersKeys
= [];
304 * Get all headers of a certain name/type
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.
311 public function get($name)
313 $key = $this->normalizeFieldName($name);
316 foreach (array_keys($this->headersKeys
, $key) as $index) {
317 if ($this->headers
[$index] instanceof Header\GenericHeader
) {
318 $results[] = $this->lazyLoadHeader($index);
320 $results[] = $this->headers
[$index];
324 switch (count($results)) {
328 if ($results[0] instanceof Header\MultipleHeadersInterface
) {
329 return new ArrayIterator($results);
335 return new ArrayIterator($results);
340 * Test for existence of a type of header
342 * @param string $name
345 public function has($name)
347 $name = $this->normalizeFieldName($name);
348 return in_array($name, $this->headersKeys
);
352 * Advance the pointer for this object as an iterator
355 public function next()
357 next($this->headers
);
361 * Return the current key for this object as an iterator
365 public function key()
367 return key($this->headers
);
371 * Is this iterator still valid?
375 public function valid()
377 return (current($this->headers
) !== false);
381 * Reset the internal pointer for this object as an iterator
384 public function rewind()
386 reset($this->headers
);
390 * Return the current value for this iterator, lazy loading it if need be
392 * @return Header\HeaderInterface
394 public function current()
396 $current = current($this->headers
);
397 if ($current instanceof Header\GenericHeader
) {
398 $current = $this->lazyLoadHeader(key($this->headers
));
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
407 * @return int count of currently known headers
409 public function count()
411 return count($this->headers
);
415 * Render all headers at once
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.
422 public function toString()
425 foreach ($this as $header) {
426 if ($str = $header->toString()) {
427 $headers .= $str . self
::EOL
;
435 * Return the headers container as an array
437 * @param bool $format Return the values in Mime::Encoded or in Raw format
439 * @todo determine how to produce single line headers, if they are supported
441 public function toArray($format = Header\HeaderInterface
::FORMAT_RAW
)
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] = [];
451 $headers[$name][] = $header->getFieldValue($format);
453 $headers[$header->getFieldName()] = $header->getFieldValue($format);
460 * By calling this, it will force parsing and loading of all headers, after this count() will be accurate
464 public function forceLoading()
466 foreach ($this as $item) {
467 // $item should now be loaded
473 * Create Header object from header line
475 * @param string $headerLine
476 * @return Header\HeaderInterface|Header\HeaderInterface[]
478 public function loadHeader($headerLine)
480 list($name, ) = Header\GenericHeader
::splitHeaderLine($headerLine);
482 /** @var HeaderInterface $class */
483 $class = $this->getPluginClassLoader()->load($name) ?
: Header\GenericHeader
::class;
484 return $class::fromString($headerLine);
491 protected function lazyLoadHeader($index)
493 $current = $this->headers
[$index];
495 $key = $this->headersKeys
[$index];
497 /** @var GenericHeader $class */
498 $class = ($this->getPluginClassLoader()->load($key)) ?
: 'Zend\Mail\Header\GenericHeader';
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;
515 $current->setEncoding($encoding);
516 $this->headers
[$index] = $current;
521 * Normalize a field name
523 * @param string $fieldName
526 protected function normalizeFieldName($fieldName)
528 return str_replace(['-', '_', ' ', '.'], '', strtolower($fieldName));