value) or string, if a content part is found it's used as toplines * - noToplines ignore content found after headers in param 'headers' * - content content as string * - strict strictly parse raw content * * @param array $params full message with or without headers * @throws Exception\InvalidArgumentException */ public function __construct(array $params) { if (isset($params['handler'])) { if (! $params['handler'] instanceof AbstractStorage) { throw new Exception\InvalidArgumentException('handler is not a valid mail handler'); } if (! isset($params['id'])) { throw new Exception\InvalidArgumentException('need a message id with a handler'); } $this->mail = $params['handler']; $this->messageNum = $params['id']; } $params['strict'] ??= false; if (isset($params['raw'])) { Mime\Decode::splitMessage( $params['raw'], $this->headers, $this->content, Mime\Mime::LINEEND, $params['strict'] ); } elseif (isset($params['headers'])) { if (is_array($params['headers'])) { $this->headers = new Headers(); $this->headers->addHeaders($params['headers']); } else { if (empty($params['noToplines'])) { Mime\Decode::splitMessage($params['headers'], $this->headers, $this->topLines); } else { $this->headers = Headers::fromString($params['headers']); } } if (isset($params['content'])) { $this->content = $params['content']; } } } /** * Check if part is a multipart message * * @return bool if part is multipart */ public function isMultipart() { try { return stripos($this->contentType, 'multipart/') === 0; } catch (Exception\ExceptionInterface) { return false; } } /** * Body of part * * If part is multipart the raw content of this part with all sub parts is returned * * @throws Exception\RuntimeException * @return string body */ public function getContent() { if ($this->content !== null) { return $this->content; } if ($this->mail) { return $this->mail->getRawContent($this->messageNum); } throw new Exception\RuntimeException('no content'); } /** * Return size of part * * Quite simple implemented currently (not decoding). Handle with care. * * @return int size */ public function getSize() { return strlen($this->getContent()); } /** * Cache content and split in parts if multipart * * @throws Exception\RuntimeException * @return void */ protected function cacheContent() { // caching content if we can't fetch parts if ($this->content === null && $this->mail) { $this->content = $this->mail->getRawContent($this->messageNum); } if (! $this->isMultipart()) { return; } // split content in parts $boundary = $this->getHeaderField('content-type', 'boundary'); if (! $boundary) { throw new Exception\RuntimeException('no boundary found in content type to split message'); } $parts = Mime\Decode::splitMessageStruct($this->content, $boundary); if ($parts === null) { return; } $counter = 1; foreach ($parts as $part) { $this->parts[$counter++] = new static(['headers' => $part['header'], 'content' => $part['body']]); } } /** * Get part of multipart message * * @param int $num number of part starting with 1 for first part * @throws Exception\RuntimeException * @return Part wanted part */ public function getPart($num) { if (isset($this->parts[$num])) { return $this->parts[$num]; } if (! $this->mail && $this->content === null) { throw new Exception\RuntimeException('part not found'); } // if ($this->mail && $this->mail->hasFetchPart) { // TODO: fetch part // return // } $this->cacheContent(); if (! isset($this->parts[$num])) { throw new Exception\RuntimeException('part not found'); } return $this->parts[$num]; } /** * Count parts of a multipart part * * @return int number of sub-parts */ public function countParts() { if ($this->countParts) { return $this->countParts; } $this->countParts = count($this->parts); if ($this->countParts) { return $this->countParts; } // if ($this->mail && $this->mail->hasFetchPart) { // TODO: fetch part // return // } $this->cacheContent(); $this->countParts = count($this->parts); return $this->countParts; } /** * Access headers collection * * Lazy-loads if not already attached. * * @return Headers * @throws Exception\RuntimeException */ public function getHeaders() { if (null === $this->headers) { if ($this->mail) { $part = $this->mail->getRawHeader($this->messageNum); $this->headers = Headers::fromString($part); } else { $this->headers = new Headers(); } } if (! $this->headers instanceof Headers) { throw new Exception\RuntimeException( '$this->headers must be an instance of Headers' ); } return $this->headers; } /** * Get a header in specified format * * Internally headers that occur more than once are saved as array, all other as string. If $format * is set to string implode is used to concat the values (with Mime::LINEEND as delim). * * @param string $name name of header, matches case-insensitive, but camel-case is replaced with dashes * @param string $format change type of return value to 'string' or 'array' * @throws Exception\InvalidArgumentException * @return string|array|HeaderInterface|ArrayIterator value of header in wanted or internal format */ public function getHeader($name, $format = null) { $header = $this->getHeaders()->get($name); if ($header === false) { $lowerName = strtolower(preg_replace('%([a-z])([A-Z])%', '\1-\2', $name)); $header = $this->getHeaders()->get($lowerName); if ($header === false) { throw new Exception\InvalidArgumentException( "Header with Name $name or $lowerName not found" ); } } switch ($format) { case 'string': if ($header instanceof HeaderInterface) { $return = $header->getFieldValue(HeaderInterface::FORMAT_RAW); } else { $return = trim(implode( Mime\Mime::LINEEND, array_map(static fn($header): string => $header->getFieldValue(HeaderInterface::FORMAT_RAW), iterator_to_array($header)) ), Mime\Mime::LINEEND); } break; case 'array': if ($header instanceof HeaderInterface) { $return = [$header->getFieldValue()]; } else { $return = []; foreach ($header as $h) { $return[] = $h->getFieldValue(HeaderInterface::FORMAT_RAW); } } break; default: $return = $header; } return $return; } /** * Get a specific field from a header like content type or all fields as array * * If the header occurs more than once, only the value from the first header * is returned. * * Throws an Exception if the requested header does not exist. If * the specific header field does not exist, returns null. * * @param string $name name of header, like in getHeader() * @param string $wantedPart the wanted part, default is first, if null an array with all parts is returned * @param string $firstName key name for the first part * @return string|array wanted part or all parts as array($firstName => firstPart, partname => value) * @throws RuntimeException */ public function getHeaderField($name, $wantedPart = '0', $firstName = '0') { return Mime\Decode::splitHeaderField(current($this->getHeader($name, 'array')), $wantedPart, $firstName); } /** * Getter for mail headers - name is matched in lowercase * * This getter is short for Part::getHeader($name, 'string') * * @see Part::getHeader() * * @param string $name header name * @return string value of header * @throws Exception\ExceptionInterface */ public function __get($name) { return $this->getHeader($name, 'string'); } /** * Isset magic method proxy to hasHeader * * This method is short syntax for Part::hasHeader($name); * * @see Part::hasHeader * * @param string $name * @return bool */ public function __isset($name) { return $this->getHeaders()->has($name); } /** * magic method to get content of part * * @return string content */ public function __toString(): string { return $this->getContent(); } /** * implements RecursiveIterator::hasChildren() * * @return bool current element has children/is multipart */ #[ReturnTypeWillChange] public function hasChildren() { $current = $this->current(); return $current && $current instanceof self && $current->isMultipart(); } /** * implements RecursiveIterator::getChildren() * * @return Part same as self::current() */ #[ReturnTypeWillChange] public function getChildren() { return $this->current(); } /** * implements Iterator::valid() * * @return bool check if there's a current element */ #[ReturnTypeWillChange] public function valid() { if ($this->countParts === null) { $this->countParts(); } return $this->iterationPos && $this->iterationPos <= $this->countParts; } /** * implements Iterator::next() */ #[ReturnTypeWillChange] public function next() { ++$this->iterationPos; } /** * implements Iterator::key() * * @return string key/number of current part */ #[ReturnTypeWillChange] public function key() { return $this->iterationPos; } /** * implements Iterator::current() * * @return Part current part */ #[ReturnTypeWillChange] public function current() { return $this->getPart($this->iterationPos); } /** * implements Iterator::rewind() */ #[ReturnTypeWillChange] public function rewind() { $this->countParts(); $this->iterationPos = 1; } }