blob: ab13eb0295033acafa52d9a19dc4b829062d2800 [file] [log] [blame]
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
use Ddeboer\Imap\Exception\ImapFetchbodyException;
use Ddeboer\Imap\Exception\UnexpectedEncodingException;
use Ddeboer\Imap\ImapResourceInterface;
use Ddeboer\Imap\Message;
/**
* A message part.
*/
abstract class AbstractPart implements PartInterface
{
/**
* @var ImapResourceInterface
*/
protected $resource;
/**
* @var bool
*/
private $structureParsed = false;
/**
* @var array
*/
private $parts = [];
/**
* @var string
*/
private $partNumber;
/**
* @var int
*/
private $messageNumber;
/**
* @var \stdClass
*/
private $structure;
/**
* @var Parameters
*/
private $parameters;
/**
* @var null|string
*/
private $type;
/**
* @var null|string
*/
private $subtype;
/**
* @var null|string
*/
private $encoding;
/**
* @var null|string
*/
private $disposition;
/**
* @var null|string
*/
private $description;
/**
* @var null|string
*/
private $bytes;
/**
* @var null|string
*/
private $lines;
/**
* @var null|string
*/
private $content;
/**
* @var null|string
*/
private $decodedContent;
/**
* @var int
*/
private $key = 0;
/**
* @var array
*/
private static $typesMap = [
\TYPETEXT => self::TYPE_TEXT,
\TYPEMULTIPART => self::TYPE_MULTIPART,
\TYPEMESSAGE => self::TYPE_MESSAGE,
\TYPEAPPLICATION => self::TYPE_APPLICATION,
\TYPEAUDIO => self::TYPE_AUDIO,
\TYPEIMAGE => self::TYPE_IMAGE,
\TYPEVIDEO => self::TYPE_VIDEO,
\TYPEMODEL => self::TYPE_MODEL,
\TYPEOTHER => self::TYPE_OTHER,
];
/**
* @var array
*/
private static $encodingsMap = [
\ENC7BIT => self::ENCODING_7BIT,
\ENC8BIT => self::ENCODING_8BIT,
\ENCBINARY => self::ENCODING_BINARY,
\ENCBASE64 => self::ENCODING_BASE64,
\ENCQUOTEDPRINTABLE => self::ENCODING_QUOTED_PRINTABLE,
];
/**
* @var array
*/
private static $attachmentKeys = [
'name' => true,
'filename' => true,
'name*' => true,
'filename*' => true,
];
/**
* Constructor.
*
* @param ImapResourceInterface $resource IMAP resource
* @param int $messageNumber Message number
* @param string $partNumber Part number
* @param \stdClass $structure Part structure
*/
public function __construct(
ImapResourceInterface $resource,
int $messageNumber,
string $partNumber,
\stdClass $structure
) {
$this->resource = $resource;
$this->messageNumber = $messageNumber;
$this->partNumber = $partNumber;
$this->setStructure($structure);
}
/**
* Get message number (from headers).
*/
final public function getNumber(): int
{
$this->assertMessageExists($this->messageNumber);
return $this->messageNumber;
}
/**
* Ensure message exists.
*/
protected function assertMessageExists(int $messageNumber): void
{
}
/**
* @param \stdClass $structure Part structure
*/
final protected function setStructure(\stdClass $structure): void
{
$this->structure = $structure;
}
/**
* Part structure.
*/
final public function getStructure(): \stdClass
{
$this->lazyLoadStructure();
return $this->structure;
}
/**
* Lazy load structure.
*/
protected function lazyLoadStructure(): void
{
}
/**
* Part parameters.
*/
final public function getParameters(): Parameters
{
$this->lazyParseStructure();
return $this->parameters;
}
/**
* Part charset.
*/
final public function getCharset(): ?string
{
$this->lazyParseStructure();
return $this->parameters->get('charset') ?: null;
}
/**
* Part type.
*/
final public function getType(): ?string
{
$this->lazyParseStructure();
return $this->type;
}
/**
* Part subtype.
*/
final public function getSubtype(): ?string
{
$this->lazyParseStructure();
return $this->subtype;
}
/**
* Part encoding.
*/
final public function getEncoding(): ?string
{
$this->lazyParseStructure();
return $this->encoding;
}
/**
* Part disposition.
*/
final public function getDisposition(): ?string
{
$this->lazyParseStructure();
return $this->disposition;
}
/**
* Part description.
*/
final public function getDescription(): ?string
{
$this->lazyParseStructure();
return $this->description;
}
/**
* Part bytes.
*
* @return null|int|string
*/
final public function getBytes()
{
$this->lazyParseStructure();
return $this->bytes;
}
/**
* Part lines.
*/
final public function getLines(): ?string
{
$this->lazyParseStructure();
return $this->lines;
}
/**
* Get raw part content.
*/
final public function getContent(): string
{
if (null === $this->content) {
$this->content = $this->doGetContent($this->getContentPartNumber());
}
return $this->content;
}
/**
* Get content part number.
*/
protected function getContentPartNumber(): string
{
return $this->partNumber;
}
/**
* Get part number.
*/
final public function getPartNumber(): string
{
return $this->partNumber;
}
/**
* Get decoded part content.
*/
final public function getDecodedContent(): string
{
if (null === $this->decodedContent) {
if (self::ENCODING_UNKNOWN === $this->getEncoding()) {
throw new UnexpectedEncodingException('Cannot decode a content with an uknown encoding');
}
$content = $this->getContent();
if (self::ENCODING_BASE64 === $this->getEncoding()) {
$content = \base64_decode($content, false);
} elseif (self::ENCODING_QUOTED_PRINTABLE === $this->getEncoding()) {
$content = \quoted_printable_decode($content);
}
if (false === $content) {
throw new UnexpectedEncodingException('Cannot decode content');
}
// If this part is a text part, convert its charset to UTF-8.
// We don't want to decode an attachment's charset.
if (!$this instanceof Attachment && null !== $this->getCharset() && self::TYPE_TEXT === $this->getType()) {
$content = Transcoder::decode($content, $this->getCharset());
}
$this->decodedContent = $content;
}
return $this->decodedContent;
}
/**
* Get raw message content.
*/
final protected function doGetContent(string $partNumber): string
{
$return = \imap_fetchbody(
$this->resource->getStream(),
$this->getNumber(),
$partNumber,
\FT_UID | \FT_PEEK
);
if (false === $return) {
throw new ImapFetchbodyException('imap_fetchbody failed');
}
return $return;
}
/**
* Get an array of all parts for this message.
*
* @return PartInterface[]
*/
final public function getParts(): array
{
$this->lazyParseStructure();
return $this->parts;
}
/**
* Get current child part.
*
* @return mixed
*/
final public function current()
{
$this->lazyParseStructure();
return $this->parts[$this->key];
}
/**
* Get current child part.
*
* @return \RecursiveIterator
*/
final public function getChildren()
{
return $this->current();
}
/**
* Get current child part.
*
* @return bool
*/
final public function hasChildren()
{
$this->lazyParseStructure();
return \count($this->parts) > 0;
}
/**
* Get current part key.
*
* @return int
*/
final public function key()
{
return $this->key;
}
/**
* Move to next part.
*
* @return void
*/
final public function next()
{
++$this->key;
}
/**
* Reset part key.
*
* @return void
*/
final public function rewind()
{
$this->key = 0;
}
/**
* Check if current part is a valid one.
*
* @return bool
*/
final public function valid()
{
$this->lazyParseStructure();
return isset($this->parts[$this->key]);
}
/**
* Parse part structure.
*/
private function lazyParseStructure(): void
{
if (true === $this->structureParsed) {
return;
}
$this->structureParsed = true;
$this->lazyLoadStructure();
$this->type = self::$typesMap[$this->structure->type] ?? self::TYPE_UNKNOWN;
// In our context, \ENCOTHER is as useful as an unknown encoding
$this->encoding = self::$encodingsMap[$this->structure->encoding] ?? self::ENCODING_UNKNOWN;
if (isset($this->structure->subtype)) {
$this->subtype = $this->structure->subtype;
}
if (isset($this->structure->bytes)) {
$this->bytes = $this->structure->bytes;
}
if ($this->structure->ifdisposition) {
$this->disposition = $this->structure->disposition;
}
if ($this->structure->ifdescription) {
$this->description = $this->structure->description;
}
$this->parameters = new Parameters();
if ($this->structure->ifparameters) {
$this->parameters->add($this->structure->parameters);
}
if ($this->structure->ifdparameters) {
$this->parameters->add($this->structure->dparameters);
}
// When the message is not multipart and the body is the attachment content
// Prevents infinite recursion
if (self::isAttachment($this->structure) && !$this instanceof Attachment) {
$this->parts[] = new Attachment($this->resource, $this->getNumber(), '1', $this->structure);
}
if (isset($this->structure->parts)) {
$parts = $this->structure->parts;
// https://secure.php.net/manual/en/function.imap-fetchbody.php#89002
if ($this instanceof Attachment && $this->isEmbeddedMessage() && 1 === \count($parts) && \TYPEMULTIPART === $parts[0]->type) {
$parts = $parts[0]->parts;
}
foreach ($parts as $key => $partStructure) {
$partNumber = (!$this instanceof Message) ? $this->partNumber . '.' : '';
$partNumber .= (string) ($key + 1);
$newPartClass = self::isAttachment($partStructure)
? Attachment::class
: SimplePart::class
;
$this->parts[] = new $newPartClass($this->resource, $this->getNumber(), $partNumber, $partStructure);
}
}
}
/**
* Check if the given part is an attachment.
*/
private static function isAttachment(\stdClass $part): bool
{
if (isset(self::$typesMap[$part->type]) && self::TYPE_MULTIPART === self::$typesMap[$part->type]) {
return false;
}
// Attachment with correct Content-Disposition header
if ($part->ifdisposition) {
if ('attachment' === \strtolower($part->disposition)) {
return true;
}
if (
'inline' === \strtolower($part->disposition)
&& self::SUBTYPE_PLAIN !== \strtoupper($part->subtype)
&& self::SUBTYPE_HTML !== \strtoupper($part->subtype)
) {
return true;
}
}
// Attachment without Content-Disposition header
if ($part->ifparameters) {
foreach ($part->parameters as $parameter) {
if (isset(self::$attachmentKeys[\strtolower($parameter->attribute)])) {
return true;
}
}
}
/*
if ($part->ifdparameters) {
foreach ($part->dparameters as $parameter) {
if (isset(self::$attachmentKeys[\strtolower($parameter->attribute)])) {
return true;
}
}
}
*/
if (self::SUBTYPE_RFC822 === \strtoupper($part->subtype)) {
return true;
}
return false;
}
}