| <?php |
| |
| declare(strict_types=1); |
| |
| namespace Ddeboer\Imap; |
| |
| use Ddeboer\Imap\Exception\ImapFetchheaderException; |
| use Ddeboer\Imap\Exception\InvalidHeadersException; |
| use Ddeboer\Imap\Exception\MessageCopyException; |
| use Ddeboer\Imap\Exception\MessageDeleteException; |
| use Ddeboer\Imap\Exception\MessageDoesNotExistException; |
| use Ddeboer\Imap\Exception\MessageMoveException; |
| use Ddeboer\Imap\Exception\MessageStructureException; |
| use Ddeboer\Imap\Exception\MessageUndeleteException; |
| |
| /** |
| * An IMAP message (e-mail). |
| */ |
| final class Message extends Message\AbstractMessage implements MessageInterface |
| { |
| /** |
| * @var bool |
| */ |
| private $messageNumberVerified = false; |
| |
| /** |
| * @var int |
| */ |
| private $imapMsgNo = 0; |
| |
| /** |
| * @var bool |
| */ |
| private $structureLoaded = false; |
| |
| /** |
| * @var null|Message\Headers |
| */ |
| private $headers; |
| |
| /** |
| * @var null|string |
| */ |
| private $rawHeaders; |
| |
| /** |
| * @var null|string |
| */ |
| private $rawMessage; |
| |
| /** |
| * Constructor. |
| * |
| * @param ImapResourceInterface $resource IMAP resource |
| * @param int $messageNumber Message number |
| */ |
| public function __construct(ImapResourceInterface $resource, int $messageNumber) |
| { |
| parent::__construct($resource, $messageNumber, '1', new \stdClass()); |
| } |
| |
| /** |
| * Lazy load structure. |
| */ |
| protected function lazyLoadStructure(): void |
| { |
| if (true === $this->structureLoaded) { |
| return; |
| } |
| $this->structureLoaded = true; |
| |
| $messageNumber = $this->getNumber(); |
| |
| $errorMessage = null; |
| $errorNumber = 0; |
| \set_error_handler(static function ($nr, $message) use (&$errorMessage, &$errorNumber): bool { |
| $errorMessage = $message; |
| $errorNumber = $nr; |
| |
| return true; |
| }); |
| |
| $structure = \imap_fetchstructure( |
| $this->resource->getStream(), |
| $messageNumber, |
| \FT_UID |
| ); |
| |
| \restore_error_handler(); |
| |
| if (!$structure instanceof \stdClass) { |
| throw new MessageStructureException(\sprintf( |
| 'Message "%s" structure is empty: %s', |
| $messageNumber, |
| $errorMessage |
| ), $errorNumber); |
| } |
| |
| $this->setStructure($structure); |
| } |
| |
| /** |
| * Ensure message exists. |
| */ |
| protected function assertMessageExists(int $messageNumber): void |
| { |
| if (true === $this->messageNumberVerified) { |
| return; |
| } |
| $this->messageNumberVerified = true; |
| |
| $msgno = null; |
| \set_error_handler(static function (): bool { |
| return true; |
| }); |
| |
| $msgno = \imap_msgno($this->resource->getStream(), $messageNumber); |
| |
| \restore_error_handler(); |
| |
| if (\is_numeric($msgno) && $msgno > 0) { |
| $this->imapMsgNo = $msgno; |
| |
| return; |
| } |
| |
| throw new MessageDoesNotExistException(\sprintf( |
| 'Message "%s" does not exist', |
| $messageNumber |
| )); |
| } |
| |
| private function getMsgNo(): int |
| { |
| // Triggers assertMessageExists() |
| $this->getNumber(); |
| |
| return $this->imapMsgNo; |
| } |
| |
| /** |
| * Get raw message headers. |
| */ |
| public function getRawHeaders(): string |
| { |
| if (null === $this->rawHeaders) { |
| $rawHeaders = \imap_fetchheader($this->resource->getStream(), $this->getNumber(), \FT_UID); |
| |
| if (false === $rawHeaders) { |
| throw new ImapFetchheaderException('imap_fetchheader failed'); |
| } |
| |
| $this->rawHeaders = $rawHeaders; |
| } |
| |
| return $this->rawHeaders; |
| } |
| |
| /** |
| * Get the raw message, including all headers, parts, etc. unencoded and unparsed. |
| * |
| * @return string the raw message |
| */ |
| public function getRawMessage(): string |
| { |
| if (null === $this->rawMessage) { |
| $this->rawMessage = $this->doGetContent(''); |
| } |
| |
| return $this->rawMessage; |
| } |
| |
| /** |
| * Get message headers. |
| */ |
| public function getHeaders(): Message\Headers |
| { |
| if (null === $this->headers) { |
| // imap_headerinfo is much faster than imap_fetchheader |
| // imap_headerinfo returns only a subset of all mail headers, |
| // but it does include the message flags. |
| $headers = \imap_headerinfo($this->resource->getStream(), $this->getMsgNo()); |
| if (false === $headers) { |
| // @see https://github.com/ddeboer/imap/issues/358 |
| throw new InvalidHeadersException(\sprintf('Message "%s" has invalid headers', $this->getNumber())); |
| } |
| $this->headers = new Message\Headers($headers); |
| } |
| |
| return $this->headers; |
| } |
| |
| /** |
| * Clearmessage headers. |
| */ |
| private function clearHeaders(): void |
| { |
| $this->headers = null; |
| } |
| |
| /** |
| * Get message recent flag value (from headers). |
| */ |
| public function isRecent(): ?string |
| { |
| return $this->getHeaders()->get('recent'); |
| } |
| |
| /** |
| * Get message unseen flag value (from headers). |
| */ |
| public function isUnseen(): bool |
| { |
| return 'U' === $this->getHeaders()->get('unseen'); |
| } |
| |
| /** |
| * Get message flagged flag value (from headers). |
| */ |
| public function isFlagged(): bool |
| { |
| return 'F' === $this->getHeaders()->get('flagged'); |
| } |
| |
| /** |
| * Get message answered flag value (from headers). |
| */ |
| public function isAnswered(): bool |
| { |
| return 'A' === $this->getHeaders()->get('answered'); |
| } |
| |
| /** |
| * Get message deleted flag value (from headers). |
| */ |
| public function isDeleted(): bool |
| { |
| return 'D' === $this->getHeaders()->get('deleted'); |
| } |
| |
| /** |
| * Get message draft flag value (from headers). |
| */ |
| public function isDraft(): bool |
| { |
| return 'X' === $this->getHeaders()->get('draft'); |
| } |
| |
| /** |
| * Has the message been marked as read? |
| */ |
| public function isSeen(): bool |
| { |
| return 'N' !== $this->getHeaders()->get('recent') && 'U' !== $this->getHeaders()->get('unseen'); |
| } |
| |
| /** |
| * Mark message as seen. |
| * |
| * @deprecated since version 1.1, to be removed in 2.0 |
| */ |
| public function maskAsSeen(): bool |
| { |
| \trigger_error(\sprintf('%s is deprecated and will be removed in 2.0. Use %s::markAsSeen instead.', __METHOD__, __CLASS__), \E_USER_DEPRECATED); |
| |
| return $this->markAsSeen(); |
| } |
| |
| /** |
| * Mark message as seen. |
| */ |
| public function markAsSeen(): bool |
| { |
| return $this->setFlag('\\Seen'); |
| } |
| |
| /** |
| * Move message to another mailbox. |
| * |
| * @throws MessageCopyException |
| */ |
| public function copy(MailboxInterface $mailbox): void |
| { |
| // 'deleted' header changed, force to reload headers, would be better to set deleted flag to true on header |
| $this->clearHeaders(); |
| |
| if (!\imap_mail_copy($this->resource->getStream(), (string) $this->getNumber(), $mailbox->getEncodedName(), \CP_UID)) { |
| throw new MessageCopyException(\sprintf('Message "%s" cannot be copied to "%s"', $this->getNumber(), $mailbox->getName())); |
| } |
| } |
| |
| /** |
| * Move message to another mailbox. |
| * |
| * @throws MessageMoveException |
| */ |
| public function move(MailboxInterface $mailbox): void |
| { |
| // 'deleted' header changed, force to reload headers, would be better to set deleted flag to true on header |
| $this->clearHeaders(); |
| |
| if (!\imap_mail_move($this->resource->getStream(), (string) $this->getNumber(), $mailbox->getEncodedName(), \CP_UID)) { |
| throw new MessageMoveException(\sprintf('Message "%s" cannot be moved to "%s"', $this->getNumber(), $mailbox->getName())); |
| } |
| } |
| |
| /** |
| * Delete message. |
| * |
| * @throws MessageDeleteException |
| */ |
| public function delete(): void |
| { |
| // 'deleted' header changed, force to reload headers, would be better to set deleted flag to true on header |
| $this->clearHeaders(); |
| |
| if (!\imap_delete($this->resource->getStream(), (string) $this->getNumber(), \FT_UID)) { |
| throw new MessageDeleteException(\sprintf('Message "%s" cannot be deleted', $this->getNumber())); |
| } |
| } |
| |
| /** |
| * Undelete message. |
| * |
| * @throws MessageUndeleteException |
| */ |
| public function undelete(): void |
| { |
| // 'deleted' header changed, force to reload headers, would be better to set deleted flag to false on header |
| $this->clearHeaders(); |
| if (!\imap_undelete($this->resource->getStream(), (string) $this->getNumber(), \FT_UID)) { |
| throw new MessageUndeleteException(\sprintf('Message "%s" cannot be undeleted', $this->getNumber())); |
| } |
| } |
| |
| /** |
| * Set Flag Message. |
| * |
| * @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft |
| */ |
| public function setFlag(string $flag): bool |
| { |
| $result = \imap_setflag_full($this->resource->getStream(), (string) $this->getNumber(), $flag, \ST_UID); |
| |
| $this->clearHeaders(); |
| |
| return $result; |
| } |
| |
| /** |
| * Clear Flag Message. |
| * |
| * @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft |
| */ |
| public function clearFlag(string $flag): bool |
| { |
| $result = \imap_clearflag_full($this->resource->getStream(), (string) $this->getNumber(), $flag, \ST_UID); |
| |
| $this->clearHeaders(); |
| |
| return $result; |
| } |
| } |