[Web] Adjust mailbox format

[Web] Include IMAP lib for future use
[Web] Fix default exception handler
[Web] Fix sync job edit forms
[Web] Other minor fixes
This commit is contained in:
andryyy
2018-11-12 10:03:50 +01:00
parent f9bfac4d27
commit d82c2bfdb7
90 changed files with 5671 additions and 56 deletions

View File

@@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use Ddeboer\Imap\Exception\CreateMailboxException;
use Ddeboer\Imap\Exception\DeleteMailboxException;
use Ddeboer\Imap\Exception\InvalidResourceException;
use Ddeboer\Imap\Exception\MailboxDoesNotExistException;
/**
* A connection to an IMAP server that is authenticated for a user.
*/
final class Connection implements ConnectionInterface
{
/**
* @var ImapResourceInterface
*/
private $resource;
/**
* @var string
*/
private $server;
/**
* @var null|array
*/
private $mailboxes;
/**
* @var null|array
*/
private $mailboxNames;
/**
* Constructor.
*
* @param ImapResourceInterface $resource
* @param string $server
*
* @throws \InvalidArgumentException
*/
public function __construct(ImapResourceInterface $resource, string $server)
{
$this->resource = $resource;
$this->server = $server;
}
/**
* Get IMAP resource.
*
* @return ImapResourceInterface
*/
public function getResource(): ImapResourceInterface
{
return $this->resource;
}
/**
* Delete all messages marked for deletion.
*
* @return bool
*/
public function expunge(): bool
{
return \imap_expunge($this->resource->getStream());
}
/**
* Close connection.
*
* @param int $flag
*
* @return bool
*/
public function close(int $flag = 0): bool
{
return \imap_close($this->resource->getStream(), $flag);
}
/**
* Get a list of mailboxes (also known as folders).
*
* @return MailboxInterface[]
*/
public function getMailboxes(): array
{
$this->initMailboxNames();
if (null === $this->mailboxes) {
$this->mailboxes = [];
foreach ($this->mailboxNames as $mailboxName => $mailboxInfo) {
$this->mailboxes[$mailboxName] = $this->getMailbox($mailboxName);
}
}
return $this->mailboxes;
}
/**
* Check that a mailbox with the given name exists.
*
* @param string $name Mailbox name
*
* @return bool
*/
public function hasMailbox(string $name): bool
{
$this->initMailboxNames();
return isset($this->mailboxNames[$name]);
}
/**
* Get a mailbox by its name.
*
* @param string $name Mailbox name
*
* @throws MailboxDoesNotExistException If mailbox does not exist
*
* @return MailboxInterface
*/
public function getMailbox(string $name): MailboxInterface
{
if (false === $this->hasMailbox($name)) {
throw new MailboxDoesNotExistException(\sprintf('Mailbox name "%s" does not exist', $name));
}
return new Mailbox($this->resource, $name, $this->mailboxNames[$name]);
}
/**
* Count number of messages not in any mailbox.
*
* @return int
*/
public function count()
{
return \imap_num_msg($this->resource->getStream());
}
/**
* Check if the connection is still active.
*
* @throws InvalidResourceException If connection was closed
*
* @return bool
*/
public function ping(): bool
{
return \imap_ping($this->resource->getStream());
}
/**
* Create mailbox.
*
* @param string $name
*
* @throws CreateMailboxException
*
* @return MailboxInterface
*/
public function createMailbox(string $name): MailboxInterface
{
if (false === \imap_createmailbox($this->resource->getStream(), $this->server . \mb_convert_encoding($name, 'UTF7-IMAP', 'UTF-8'))) {
throw new CreateMailboxException(\sprintf('Can not create "%s" mailbox at "%s"', $name, $this->server));
}
$this->mailboxNames = $this->mailboxes = null;
$this->resource->clearLastMailboxUsedCache();
return $this->getMailbox($name);
}
/**
* Create mailbox.
*
* @param MailboxInterface $mailbox
*
* @throws DeleteMailboxException
*/
public function deleteMailbox(MailboxInterface $mailbox)
{
if (false === \imap_deletemailbox($this->resource->getStream(), $mailbox->getFullEncodedName())) {
throw new DeleteMailboxException(\sprintf('Mailbox "%s" could not be deleted', $mailbox->getName()));
}
$this->mailboxes = $this->mailboxNames = null;
$this->resource->clearLastMailboxUsedCache();
}
/**
* Get mailbox names.
*/
private function initMailboxNames()
{
if (null !== $this->mailboxNames) {
return;
}
$this->mailboxNames = [];
$mailboxesInfo = \imap_getmailboxes($this->resource->getStream(), $this->server, '*');
foreach ($mailboxesInfo as $mailboxInfo) {
$name = \mb_convert_encoding(\str_replace($this->server, '', $mailboxInfo->name), 'UTF-8', 'UTF7-IMAP');
$this->mailboxNames[$name] = $mailboxInfo;
}
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
/**
* A connection to an IMAP server that is authenticated for a user.
*/
interface ConnectionInterface extends \Countable
{
/**
* Get IMAP resource.
*
* @return ImapResourceInterface
*/
public function getResource(): ImapResourceInterface;
/**
* Delete all messages marked for deletion.
*
* @return bool
*/
public function expunge(): bool;
/**
* Close connection.
*
* @param int $flag
*
* @return bool
*/
public function close(int $flag = 0): bool;
/**
* Check if the connection is still active.
*
* @return bool
*/
public function ping(): bool;
/**
* Get a list of mailboxes (also known as folders).
*
* @return MailboxInterface[]
*/
public function getMailboxes(): array;
/**
* Check that a mailbox with the given name exists.
*
* @param string $name Mailbox name
*
* @return bool
*/
public function hasMailbox(string $name): bool;
/**
* Get a mailbox by its name.
*
* @param string $name Mailbox name
*
* @return MailboxInterface
*/
public function getMailbox(string $name): MailboxInterface;
/**
* Create mailbox.
*
* @param string $name
*
* @return MailboxInterface
*/
public function createMailbox(string $name): MailboxInterface;
/**
* Create mailbox.
*
* @param MailboxInterface $mailbox
*/
public function deleteMailbox(MailboxInterface $mailbox);
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
abstract class AbstractException extends \RuntimeException
{
/**
* @var array
*/
private static $errorLabels = [
\E_ERROR => 'E_ERROR',
\E_WARNING => 'E_WARNING',
\E_PARSE => 'E_PARSE',
\E_NOTICE => 'E_NOTICE',
\E_CORE_ERROR => 'E_CORE_ERROR',
\E_CORE_WARNING => 'E_CORE_WARNING',
\E_COMPILE_ERROR => 'E_COMPILE_ERROR',
\E_COMPILE_WARNING => 'E_COMPILE_WARNING',
\E_USER_ERROR => 'E_USER_ERROR',
\E_USER_WARNING => 'E_USER_WARNING',
\E_USER_NOTICE => 'E_USER_NOTICE',
\E_STRICT => 'E_STRICT',
\E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR',
\E_DEPRECATED => 'E_DEPRECATED',
\E_USER_DEPRECATED => 'E_USER_DEPRECATED',
];
/**
* @param string $message The exception message
* @param int $code The exception code
* @param \Throwable $previous The previous exception
*/
final public function __construct(string $message, int $code = 0, \Throwable $previous = null)
{
$errorType = '';
if (\is_int($code) && isset(self::$errorLabels[$code])) {
$errorType = \sprintf('[%s] ', self::$errorLabels[$code]);
}
$joinString = "\n- ";
$alerts = \imap_alerts();
$errors = \imap_errors();
$completeMessage = \sprintf(
"%s%s\nimap_alerts (%s):%s\nimap_errors (%s):%s",
$errorType,
$message,
$alerts ? \count($alerts) : 0,
$alerts ? $joinString . \implode($joinString, $alerts) : '',
$errors ? \count($errors) : 0,
$errors ? $joinString . \implode($joinString, $errors) : ''
);
parent::__construct($completeMessage, $code, $previous);
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class AuthenticationFailedException extends AbstractException
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class CreateMailboxException extends AbstractException
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class DeleteMailboxException extends AbstractException
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class InvalidDateHeaderException extends AbstractException
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class InvalidHeadersException extends AbstractException
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class InvalidResourceException extends AbstractException
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class InvalidSearchCriteriaException extends AbstractException
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class MailboxDoesNotExistException extends AbstractException
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class MessageCopyException extends AbstractException
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class MessageDeleteException extends AbstractException
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class MessageDoesNotExistException extends AbstractException
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class MessageMoveException extends AbstractException
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class MessageStructureException extends AbstractException
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class NotEmbeddedMessageException extends AbstractException
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class OutOfBoundsException extends AbstractException
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class ReopenMailboxException extends AbstractException
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class UnexpectedEncodingException extends AbstractException
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class UnsupportedCharsetException extends AbstractException
{
}

View File

@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use Ddeboer\Imap\Exception\InvalidResourceException;
use Ddeboer\Imap\Exception\ReopenMailboxException;
/**
* An imap resource stream.
*/
final class ImapResource implements ImapResourceInterface
{
/**
* @var resource
*/
private $resource;
/**
* @var null|MailboxInterface
*/
private $mailbox;
/**
* @var null|string
*/
private static $lastMailboxUsedCache;
/**
* Constructor.
*
* @param resource $resource
*/
public function __construct($resource, MailboxInterface $mailbox = null)
{
$this->resource = $resource;
$this->mailbox = $mailbox;
}
/**
* Get IMAP resource stream.
*
* @throws InvalidResourceException
*
* @return resource
*/
public function getStream()
{
if (false === \is_resource($this->resource) || 'imap' !== \get_resource_type($this->resource)) {
throw new InvalidResourceException('Supplied resource is not a valid imap resource');
}
$this->initMailbox();
return $this->resource;
}
/**
* Clear last mailbox used cache.
*/
public function clearLastMailboxUsedCache()
{
self::$lastMailboxUsedCache = null;
}
/**
* If connection is not currently in this mailbox, switch it to this mailbox.
*/
private function initMailbox()
{
if (null === $this->mailbox || $this->isMailboxOpen()) {
return;
}
\imap_reopen($this->resource, $this->mailbox->getFullEncodedName());
if ($this->isMailboxOpen()) {
return;
}
throw new ReopenMailboxException(\sprintf('Cannot reopen mailbox "%s"', $this->mailbox->getName()));
}
/**
* Check whether the current mailbox is open.
*
* @return bool
*/
private function isMailboxOpen(): bool
{
$currentMailboxName = $this->mailbox->getFullEncodedName();
if ($currentMailboxName === self::$lastMailboxUsedCache) {
return true;
}
self::$lastMailboxUsedCache = null;
$check = \imap_check($this->resource);
$return = false !== $check && $check->Mailbox === $currentMailboxName;
if (true === $return) {
self::$lastMailboxUsedCache = $currentMailboxName;
}
return $return;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
interface ImapResourceInterface
{
/**
* Get IMAP resource stream.
*
* @return resource
*/
public function getStream();
/**
* Clear last mailbox used cache.
*/
public function clearLastMailboxUsedCache();
}

View File

@@ -0,0 +1,294 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use DateTimeInterface;
use Ddeboer\Imap\Exception\InvalidSearchCriteriaException;
use Ddeboer\Imap\Exception\MessageCopyException;
use Ddeboer\Imap\Exception\MessageMoveException;
use Ddeboer\Imap\Search\ConditionInterface;
use Ddeboer\Imap\Search\LogicalOperator\All;
/**
* An IMAP mailbox (commonly referred to as a 'folder').
*/
final class Mailbox implements MailboxInterface
{
/**
* @var ImapResourceInterface
*/
private $resource;
/**
* @var string
*/
private $name;
/**
* @var \stdClass
*/
private $info;
/**
* Constructor.
*
* @param ImapResourceInterface $resource IMAP resource
* @param string $name Mailbox decoded name
* @param \stdClass $info Mailbox info
*/
public function __construct(ImapResourceInterface $resource, string $name, \stdClass $info)
{
$this->resource = new ImapResource($resource->getStream(), $this);
$this->name = $name;
$this->info = $info;
}
/**
* Get mailbox decoded name.
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* Get mailbox encoded path.
*
* @return string
*/
public function getEncodedName(): string
{
return \preg_replace('/^{.+}/', '', $this->info->name);
}
/**
* Get mailbox encoded full name.
*
* @return string
*/
public function getFullEncodedName(): string
{
return $this->info->name;
}
/**
* Get mailbox attributes.
*
* @return int
*/
public function getAttributes(): int
{
return $this->info->attributes;
}
/**
* Get mailbox delimiter.
*
* @return string
*/
public function getDelimiter(): string
{
return $this->info->delimiter;
}
/**
* Get number of messages in this mailbox.
*
* @return int
*/
public function count()
{
return \imap_num_msg($this->resource->getStream());
}
/**
* Get Mailbox status.
*
* @param null|int $flags
*
* @return \stdClass
*/
public function getStatus(int $flags = null): \stdClass
{
return \imap_status($this->resource->getStream(), $this->getFullEncodedName(), $flags ?? \SA_ALL);
}
/**
* Bulk Set Flag for Messages.
*
* @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
* @param array|MessageIterator|string $numbers Message numbers
*
* @return bool
*/
public function setFlag(string $flag, $numbers): bool
{
return \imap_setflag_full($this->resource->getStream(), $this->prepareMessageIds($numbers), $flag, \ST_UID);
}
/**
* Bulk Clear Flag for Messages.
*
* @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
* @param array|MessageIterator|string $numbers Message numbers
*
* @return bool
*/
public function clearFlag(string $flag, $numbers): bool
{
return \imap_clearflag_full($this->resource->getStream(), $this->prepareMessageIds($numbers), $flag, \ST_UID);
}
/**
* Get message ids.
*
* @param ConditionInterface $search Search expression (optional)
*
* @return MessageIteratorInterface
*/
public function getMessages(ConditionInterface $search = null, int $sortCriteria = null, bool $descending = false): MessageIteratorInterface
{
if (null === $search) {
$search = new All();
}
$query = $search->toString();
// We need to clear the stack to know whether imap_last_error()
// is related to this imap_search
\imap_errors();
if (null !== $sortCriteria) {
$messageNumbers = \imap_sort($this->resource->getStream(), $sortCriteria, $descending ? 1 : 0, \SE_UID, $query);
} else {
$messageNumbers = \imap_search($this->resource->getStream(), $query, \SE_UID);
}
if (false === $messageNumbers) {
if (false !== \imap_last_error()) {
throw new InvalidSearchCriteriaException(\sprintf('Invalid search criteria [%s]', $query));
}
// imap_search can also return false
$messageNumbers = [];
}
return new MessageIterator($this->resource, $messageNumbers);
}
/**
* Get a message by message number.
*
* @param int $number Message number
*
* @return MessageInterface
*/
public function getMessage(int $number): MessageInterface
{
return new Message($this->resource, $number);
}
/**
* Get messages in this mailbox.
*
* @return MessageIteratorInterface
*/
public function getIterator(): MessageIteratorInterface
{
return $this->getMessages();
}
/**
* Add a message to the mailbox.
*
* @param string $message
* @param null|string $options
* @param null|DateTimeInterface $internalDate
*
* @return bool
*/
public function addMessage(string $message, string $options = null, DateTimeInterface $internalDate = null): bool
{
$arguments = [
$this->resource->getStream(),
$this->getFullEncodedName(),
$message,
];
if (null !== $options) {
$arguments[] = $options;
if (null !== $internalDate) {
$arguments[] = $internalDate->format('d-M-Y H:i:s O');
}
}
return \imap_append(...$arguments);
}
/**
* Returns a tree of threaded message for the current Mailbox.
*
* @return array
*/
public function getThread(): array
{
\set_error_handler(function () {});
$tree = \imap_thread($this->resource->getStream());
\restore_error_handler();
return false !== $tree ? $tree : [];
}
/**
* Bulk move messages.
*
* @param array|MessageIterator|string $numbers Message numbers
* @param MailboxInterface $mailbox Destination Mailbox to move the messages to
*
* @throws \Ddeboer\Imap\Exception\MessageMoveException
*/
public function move($numbers, MailboxInterface $mailbox)
{
if (!\imap_mail_move($this->resource->getStream(), $this->prepareMessageIds($numbers), $mailbox->getEncodedName(), \CP_UID)) {
throw new MessageMoveException(\sprintf('Messages cannot be moved to "%s"', $mailbox->getName()));
}
}
/**
* Bulk copy messages.
*
* @param array|MessageIterator|string $numbers Message numbers
* @param MailboxInterface $mailbox Destination Mailbox to copy the messages to
*
* @throws \Ddeboer\Imap\Exception\MessageCopyException
*/
public function copy($numbers, MailboxInterface $mailbox)
{
if (!\imap_mail_copy($this->resource->getStream(), $this->prepareMessageIds($numbers), $mailbox->getEncodedName(), \CP_UID)) {
throw new MessageCopyException(\sprintf('Messages cannot be copied to "%s"', $mailbox->getName()));
}
}
/**
* Prepare message ids for the use with bulk functions.
*
* @param array|MessageIterator|string $messageIds Message numbers
*
* @return string
*/
private function prepareMessageIds($messageIds): string
{
if ($messageIds instanceof MessageIterator) {
$messageIds = $messageIds->getArrayCopy();
}
if (\is_array($messageIds)) {
$messageIds = \implode(',', $messageIds);
}
return (string) $messageIds;
}
}

View File

@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use DateTimeInterface;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* An IMAP mailbox (commonly referred to as a 'folder').
*/
interface MailboxInterface extends \Countable, \IteratorAggregate
{
/**
* Get mailbox decoded name.
*
* @return string
*/
public function getName(): string;
/**
* Get mailbox encoded path.
*
* @return string
*/
public function getEncodedName(): string;
/**
* Get mailbox encoded full name.
*
* @return string
*/
public function getFullEncodedName(): string;
/**
* Get mailbox attributes.
*
* @return int
*/
public function getAttributes(): int;
/**
* Get mailbox delimiter.
*
* @return string
*/
public function getDelimiter(): string;
/**
* Get Mailbox status.
*
* @param null|int $flags
*
* @return \stdClass
*/
public function getStatus(int $flags = null): \stdClass;
/**
* Bulk Set Flag for Messages.
*
* @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
* @param array|MessageIterator|string $numbers Message numbers
*
* @return bool
*/
public function setFlag(string $flag, $numbers): bool;
/**
* Bulk Clear Flag for Messages.
*
* @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
* @param array|MessageIterator|string $numbers Message numbers
*
* @return bool
*/
public function clearFlag(string $flag, $numbers): bool;
/**
* Get message ids.
*
* @param ConditionInterface $search Search expression (optional)
*
* @return MessageIteratorInterface
*/
public function getMessages(ConditionInterface $search = null, int $sortCriteria = null, bool $descending = false): MessageIteratorInterface;
/**
* Get a message by message number.
*
* @param int $number Message number
*
* @return MessageInterface
*/
public function getMessage(int $number): MessageInterface;
/**
* Get messages in this mailbox.
*
* @return MessageIteratorInterface
*/
public function getIterator(): MessageIteratorInterface;
/**
* Add a message to the mailbox.
*
* @param string $message
* @param null|string $options
* @param null|DateTimeInterface $internalDate
*
* @return bool
*/
public function addMessage(string $message, string $options = null, DateTimeInterface $internalDate = null): bool;
/**
* Returns a tree of threaded message for the current Mailbox.
*
* @return array
*/
public function getThread(): array;
/**
* Bulk move messages.
*
* @param array|MessageIterator|string $numbers Message numbers
* @param MailboxInterface $mailbox Destination Mailbox to move the messages to
*
* @throws \Ddeboer\Imap\Exception\MessageMoveException
*/
public function move($numbers, self $mailbox);
/**
* Bulk copy messages.
*
* @param array|MessageIterator|string $numbers Message numbers
* @param MailboxInterface $mailbox Destination Mailbox to copy the messages to
*
* @throws \Ddeboer\Imap\Exception\MessageCopyException
*/
public function copy($numbers, self $mailbox);
}

View File

@@ -0,0 +1,348 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
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;
/**
* An IMAP message (e-mail).
*/
final class Message extends Message\AbstractMessage implements MessageInterface
{
/**
* @var bool
*/
private $messageNumberVerified = false;
/**
* @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()
{
if (true === $this->structureLoaded) {
return;
}
$this->structureLoaded = true;
$messageNumber = $this->getNumber();
$errorMessage = null;
$errorNumber = 0;
\set_error_handler(function ($nr, $message) use (&$errorMessage, &$errorNumber) {
$errorMessage = $message;
$errorNumber = $nr;
});
$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.
*
* @param int $messageNumber
*/
protected function assertMessageExists(int $messageNumber)
{
if (true === $this->messageNumberVerified) {
return;
}
$this->messageNumberVerified = true;
$msgno = \imap_msgno($this->resource->getStream(), $messageNumber);
if (\is_numeric($msgno) && $msgno > 0) {
return;
}
throw new MessageDoesNotExistException(\sprintf(
'Message "%s" does not exist',
$messageNumber
));
}
/**
* Get raw message headers.
*
* @return string
*/
public function getRawHeaders(): string
{
if (null === $this->rawHeaders) {
$this->rawHeaders = \imap_fetchheader($this->resource->getStream(), $this->getNumber(), \FT_UID);
}
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.
*
* @return 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(), \imap_msgno($this->resource->getStream(), $this->getNumber()));
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()
{
$this->headers = null;
}
/**
* Get message recent flag value (from headers).
*
* @return null|string
*/
public function isRecent()
{
return $this->getHeaders()->get('recent');
}
/**
* Get message unseen flag value (from headers).
*
* @return bool
*/
public function isUnseen(): bool
{
return 'U' === $this->getHeaders()->get('unseen');
}
/**
* Get message flagged flag value (from headers).
*
* @return bool
*/
public function isFlagged(): bool
{
return 'F' === $this->getHeaders()->get('flagged');
}
/**
* Get message answered flag value (from headers).
*
* @return bool
*/
public function isAnswered(): bool
{
return 'A' === $this->getHeaders()->get('answered');
}
/**
* Get message deleted flag value (from headers).
*
* @return bool
*/
public function isDeleted(): bool
{
return 'D' === $this->getHeaders()->get('deleted');
}
/**
* Get message draft flag value (from headers).
*
* @return bool
*/
public function isDraft(): bool
{
return 'X' === $this->getHeaders()->get('draft');
}
/**
* Has the message been marked as read?
*
* @return bool
*/
public function isSeen(): bool
{
return 'N' !== $this->getHeaders()->get('recent') && 'U' !== $this->getHeaders()->get('unseen');
}
/**
* Mark message as seen.
*
* @return bool
*
* @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.
*
* @return bool
*/
public function markAsSeen(): bool
{
return $this->setFlag('\\Seen');
}
/**
* Move message to another mailbox.
*
* @param MailboxInterface $mailbox
*
* @throws MessageCopyException
*/
public function copy(MailboxInterface $mailbox)
{
// '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.
*
* @param MailboxInterface $mailbox
*
* @throws MessageMoveException
*/
public function move(MailboxInterface $mailbox)
{
// '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()
{
// '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(), $this->getNumber(), \FT_UID)) {
throw new MessageDeleteException(\sprintf('Message "%s" cannot be deleted', $this->getNumber()));
}
}
/**
* Set Flag Message.
*
* @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
*
* @return bool
*/
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
*
* @return bool
*/
public function clearFlag(string $flag): bool
{
$result = \imap_clearflag_full($this->resource->getStream(), (string) $this->getNumber(), $flag, \ST_UID);
$this->clearHeaders();
return $result;
}
}

View File

@@ -0,0 +1,292 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
use Ddeboer\Imap\Exception\InvalidDateHeaderException;
abstract class AbstractMessage extends AbstractPart
{
/**
* @var null|array
*/
private $attachments;
/**
* Get message headers.
*
* @return Headers
*/
abstract public function getHeaders(): Headers;
/**
* Get message id.
*
* A unique message id in the form <...>
*
* @return null|string
*/
final public function getId()
{
return $this->getHeaders()->get('message_id');
}
/**
* Get message sender (from headers).
*
* @return null|EmailAddress
*/
final public function getFrom()
{
$from = $this->getHeaders()->get('from');
return null !== $from ? $this->decodeEmailAddress($from[0]) : null;
}
/**
* Get To recipients.
*
* @return EmailAddress[] Empty array in case message has no To: recipients
*/
final public function getTo(): array
{
return $this->decodeEmailAddresses($this->getHeaders()->get('to') ?: []);
}
/**
* Get Cc recipients.
*
* @return EmailAddress[] Empty array in case message has no CC: recipients
*/
final public function getCc(): array
{
return $this->decodeEmailAddresses($this->getHeaders()->get('cc') ?: []);
}
/**
* Get Bcc recipients.
*
* @return EmailAddress[] Empty array in case message has no BCC: recipients
*/
final public function getBcc(): array
{
return $this->decodeEmailAddresses($this->getHeaders()->get('bcc') ?: []);
}
/**
* Get Reply-To recipients.
*
* @return EmailAddress[] Empty array in case message has no Reply-To: recipients
*/
final public function getReplyTo(): array
{
return $this->decodeEmailAddresses($this->getHeaders()->get('reply_to') ?: []);
}
/**
* Get Sender.
*
* @return EmailAddress[] Empty array in case message has no Sender: recipients
*/
final public function getSender(): array
{
return $this->decodeEmailAddresses($this->getHeaders()->get('sender') ?: []);
}
/**
* Get Return-Path.
*
* @return EmailAddress[] Empty array in case message has no Return-Path: recipients
*/
final public function getReturnPath(): array
{
return $this->decodeEmailAddresses($this->getHeaders()->get('return_path') ?: []);
}
/**
* Get date (from headers).
*
* @return null|\DateTimeImmutable
*/
final public function getDate()
{
$dateHeader = $this->getHeaders()->get('date');
if (null === $dateHeader) {
return null;
}
$alteredValue = $dateHeader;
$alteredValue = \str_replace(',', '', $alteredValue);
$alteredValue = \preg_replace('/^[a-zA-Z]+ ?/', '', $alteredValue);
$alteredValue = \preg_replace('/ +\(.*\)/', '', $alteredValue);
$alteredValue = \preg_replace('/\bUT\b/', 'UTC', $alteredValue);
if (0 === \preg_match('/\d\d:\d\d:\d\d.* [\+\-]\d\d:?\d\d/', $alteredValue)) {
$alteredValue .= ' +0000';
}
try {
$date = new \DateTimeImmutable($alteredValue);
} catch (\Throwable $ex) {
throw new InvalidDateHeaderException(\sprintf('Invalid Date header found: "%s"', $dateHeader), 0, $ex);
}
return $date;
}
/**
* Get message size (from headers).
*
* @return null|int|string
*/
final public function getSize()
{
return $this->getHeaders()->get('size');
}
/**
* Get message subject (from headers).
*
* @return null|string
*/
final public function getSubject()
{
return $this->getHeaders()->get('subject');
}
/**
* Get message In-Reply-To (from headers).
*
* @return array
*/
final public function getInReplyTo(): array
{
$inReplyTo = $this->getHeaders()->get('in_reply_to');
return null !== $inReplyTo ? \explode(' ', $inReplyTo) : [];
}
/**
* Get message References (from headers).
*
* @return array
*/
final public function getReferences(): array
{
$references = $this->getHeaders()->get('references');
return null !== $references ? \explode(' ', $references) : [];
}
/**
* Get body HTML.
*
* @return null|string
*/
final public function getBodyHtml()
{
$iterator = new \RecursiveIteratorIterator($this, \RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $part) {
if (self::SUBTYPE_HTML === $part->getSubtype()) {
return $part->getDecodedContent();
}
}
// If message has no parts and is HTML, return content of message itself.
if (self::SUBTYPE_HTML === $this->getSubtype()) {
return $this->getDecodedContent();
}
return null;
}
/**
* Get body text.
*
* @return null|string
*/
final public function getBodyText()
{
$iterator = new \RecursiveIteratorIterator($this, \RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $part) {
if (self::SUBTYPE_PLAIN === $part->getSubtype()) {
return $part->getDecodedContent();
}
}
// If message has no parts, return content of message itself.
if (self::SUBTYPE_PLAIN === $this->getSubtype()) {
return $this->getDecodedContent();
}
return null;
}
/**
* Get attachments (if any) linked to this e-mail.
*
* @return AttachmentInterface[]
*/
final public function getAttachments(): array
{
if (null === $this->attachments) {
static $gatherAttachments;
if (null === $gatherAttachments) {
$gatherAttachments = static function (PartInterface $part) use (&$gatherAttachments): array {
$attachments = [];
foreach ($part->getParts() as $childPart) {
if ($childPart instanceof Attachment) {
$attachments[] = $childPart;
}
if ($childPart->hasChildren()) {
$attachments = \array_merge($attachments, $gatherAttachments($childPart));
}
}
return $attachments;
};
}
$this->attachments = $gatherAttachments($this);
}
return $this->attachments;
}
/**
* Does this message have attachments?
*
* @return bool
*/
final public function hasAttachments(): bool
{
return \count($this->getAttachments()) > 0;
}
/**
* @param array $addresses Addesses
*
* @return array
*/
private function decodeEmailAddresses(array $addresses): array
{
$return = [];
foreach ($addresses as $address) {
if (isset($address->mailbox)) {
$return[] = $this->decodeEmailAddress($address);
}
}
return $return;
}
/**
* @param \stdClass $value
*
* @return EmailAddress
*/
private function decodeEmailAddress(\stdClass $value): EmailAddress
{
return new EmailAddress($value->mailbox, $value->host, $value->personal);
}
}

View File

@@ -0,0 +1,578 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
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 $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).
*
* @return int
*/
final public function getNumber(): int
{
$this->assertMessageExists($this->messageNumber);
return $this->messageNumber;
}
/**
* Ensure message exists.
*
* @param int $messageNumber
*/
protected function assertMessageExists(int $messageNumber)
{
}
/**
* @param \stdClass $structure Part structure
*/
final protected function setStructure(\stdClass $structure)
{
$this->structure = $structure;
}
/**
* Part structure.
*
* @return \stdClass
*/
final public function getStructure(): \stdClass
{
$this->lazyLoadStructure();
return $this->structure;
}
/**
* Lazy load structure.
*/
protected function lazyLoadStructure()
{
}
/**
* Part parameters.
*
* @return Parameters
*/
final public function getParameters(): Parameters
{
$this->lazyParseStructure();
return $this->parameters;
}
/**
* Part charset.
*
* @return null|string
*/
final public function getCharset()
{
$this->lazyParseStructure();
return $this->parameters->get('charset') ?: null;
}
/**
* Part type.
*
* @return null|string
*/
final public function getType()
{
$this->lazyParseStructure();
return $this->type;
}
/**
* Part subtype.
*
* @return null|string
*/
final public function getSubtype()
{
$this->lazyParseStructure();
return $this->subtype;
}
/**
* Part encoding.
*
* @return null|string
*/
final public function getEncoding()
{
$this->lazyParseStructure();
return $this->encoding;
}
/**
* Part disposition.
*
* @return null|string
*/
final public function getDisposition()
{
$this->lazyParseStructure();
return $this->disposition;
}
/**
* Part bytes.
*
* @return null|string
*/
final public function getBytes()
{
$this->lazyParseStructure();
return $this->bytes;
}
/**
* Part lines.
*
* @return null|string
*/
final public function getLines()
{
$this->lazyParseStructure();
return $this->lines;
}
/**
* Get raw part content.
*
* @return string
*/
final public function getContent(): string
{
if (null === $this->content) {
$this->content = $this->doGetContent($this->getContentPartNumber());
}
return $this->content;
}
/**
* Get content part number.
*
* @return string
*/
protected function getContentPartNumber(): string
{
return $this->partNumber;
}
/**
* Get part number.
*
* @return string
*/
final public function getPartNumber(): string
{
return $this->partNumber;
}
/**
* Get decoded part content.
*
* @return string
*/
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);
} 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.
*
* @param string $partNumber
*
* @return string
*/
final protected function doGetContent(string $partNumber): string
{
return \imap_fetchbody(
$this->resource->getStream(),
$this->getNumber(),
$partNumber,
\FT_UID | \FT_PEEK
);
}
/**
* 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 mixed
*/
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 int
*/
final public function next()
{
++$this->key;
}
/**
* Reset part key.
*
* @return int
*/
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()
{
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 uknown encoding
$this->encoding = self::$encodingsMap[$this->structure->encoding] ?? self::ENCODING_UNKNOWN;
$this->subtype = $this->structure->subtype;
foreach (['disposition', 'bytes', 'description'] as $optional) {
if (isset($this->structure->{$optional})) {
$this->{$optional} = $this->structure->{$optional};
}
}
$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.
*
* @param \stdClass $part
*
* @return bool
*/
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;
}
}
}
*/
return false;
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
use Ddeboer\Imap\Exception\NotEmbeddedMessageException;
/**
* An e-mail attachment.
*/
final class Attachment extends AbstractPart implements AttachmentInterface
{
/**
* Get attachment filename.
*
* @return null|string
*/
public function getFilename()
{
return $this->getParameters()->get('filename')
?: $this->getParameters()->get('name');
}
/**
* Get attachment file size.
*
* @return int Number of bytes
*/
public function getSize()
{
return $this->getParameters()->get('size');
}
/**
* Is this attachment also an Embedded Message?
*
* @return bool
*/
public function isEmbeddedMessage(): bool
{
return self::TYPE_MESSAGE === $this->getType();
}
/**
* Return embedded message.
*
* @throws NotEmbeddedMessageException
*
* @return EmbeddedMessageInterface
*/
public function getEmbeddedMessage(): EmbeddedMessageInterface
{
if (!$this->isEmbeddedMessage()) {
throw new NotEmbeddedMessageException(\sprintf(
'Attachment "%s" in message "%s" is not embedded message',
$this->getPartNumber(),
$this->getNumber()
));
}
return new EmbeddedMessage($this->resource, $this->getNumber(), $this->getPartNumber(), $this->getStructure()->parts[0]);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
/**
* An e-mail attachment.
*/
interface AttachmentInterface extends PartInterface
{
/**
* Get attachment filename.
*
* @return null|string
*/
public function getFilename();
/**
* Get attachment file size.
*
* @return int Number of bytes
*/
public function getSize();
/**
* Is this attachment also an Embedded Message?
*
* @return bool
*/
public function isEmbeddedMessage(): bool;
/**
* Return embedded message.
*
* @return EmbeddedMessageInterface
*/
public function getEmbeddedMessage(): EmbeddedMessageInterface;
}

View File

@@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
interface BasicMessageInterface extends PartInterface
{
/**
* Get raw message headers.
*
* @return string
*/
public function getRawHeaders(): string;
/**
* Get the raw message, including all headers, parts, etc. unencoded and unparsed.
*
* @return string the raw message
*/
public function getRawMessage(): string;
/**
* Get message headers.
*
* @return Headers
*/
public function getHeaders(): Headers;
/**
* Get message id.
*
* A unique message id in the form <...>
*
* @return null|string
*/
public function getId();
/**
* Get message sender (from headers).
*
* @return null|EmailAddress
*/
public function getFrom();
/**
* Get To recipients.
*
* @return EmailAddress[] Empty array in case message has no To: recipients
*/
public function getTo(): array;
/**
* Get Cc recipients.
*
* @return EmailAddress[] Empty array in case message has no CC: recipients
*/
public function getCc(): array;
/**
* Get Bcc recipients.
*
* @return EmailAddress[] Empty array in case message has no BCC: recipients
*/
public function getBcc(): array;
/**
* Get Reply-To recipients.
*
* @return EmailAddress[] Empty array in case message has no Reply-To: recipients
*/
public function getReplyTo(): array;
/**
* Get Sender.
*
* @return EmailAddress[] Empty array in case message has no Sender: recipients
*/
public function getSender(): array;
/**
* Get Return-Path.
*
* @return EmailAddress[] Empty array in case message has no Return-Path: recipients
*/
public function getReturnPath(): array;
/**
* Get date (from headers).
*
* @return null|\DateTimeImmutable
*/
public function getDate();
/**
* Get message size (from headers).
*
* @return int
*/
public function getSize();
/**
* Get message subject (from headers).
*
* @return string
*/
public function getSubject();
/**
* Get message In-Reply-To (from headers).
*
* @return array
*/
public function getInReplyTo(): array;
/**
* Get message References (from headers).
*
* @return array
*/
public function getReferences(): array;
/**
* Get body HTML.
*
* @return string | null Null if message has no HTML message part
*/
public function getBodyHtml();
/**
* Get body text.
*
* @return string
*/
public function getBodyText();
/**
* Get attachments (if any) linked to this e-mail.
*
* @return AttachmentInterface[]
*/
public function getAttachments(): array;
/**
* Does this message have attachments?
*
* @return bool
*/
public function hasAttachments(): bool;
}

View File

@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
/**
* An e-mail address.
*/
final class EmailAddress
{
/**
* @var string
*/
private $mailbox;
/**
* @var null|string
*/
private $hostname;
/**
* @var null|string
*/
private $name;
/**
* @var null|string
*/
private $address;
/**
* @param string $mailbox
* @param null|string $hostname
* @param null|string $name
*/
public function __construct(string $mailbox, string $hostname = null, string $name = null)
{
$this->mailbox = $mailbox;
$this->hostname = $hostname;
$this->name = $name;
if (null !== $hostname) {
$this->address = $mailbox . '@' . $hostname;
}
}
/**
* @return null|string
*/
public function getAddress()
{
return $this->address;
}
/**
* Returns address with person name.
*
* @return string
*/
public function getFullAddress(): string
{
$address = \sprintf('%s@%s', $this->mailbox, $this->hostname);
if (null !== $this->name) {
$address = \sprintf('"%s" <%s>', \addcslashes($this->name, '"'), $address);
}
return $address;
}
/**
* @return string
*/
public function getMailbox(): string
{
return $this->mailbox;
}
/**
* @return null|string
*/
public function getHostname()
{
return $this->hostname;
}
/**
* @return null|string
*/
public function getName()
{
return $this->name;
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
final class EmbeddedMessage extends AbstractMessage implements EmbeddedMessageInterface
{
/**
* @var null|Headers
*/
private $headers;
/**
* @var null|string
*/
private $rawHeaders;
/**
* @var null|string
*/
private $rawMessage;
/**
* Get message headers.
*
* @return Headers
*/
public function getHeaders(): Headers
{
if (null === $this->headers) {
$this->headers = new Headers(\imap_rfc822_parse_headers($this->getRawHeaders()));
}
return $this->headers;
}
/**
* Get raw message headers.
*
* @return string
*/
public function getRawHeaders(): string
{
if (null === $this->rawHeaders) {
$rawHeaders = \explode("\r\n\r\n", $this->getRawMessage(), 2);
$this->rawHeaders = \current($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($this->getPartNumber());
}
return $this->rawMessage;
}
/**
* Get content part number.
*
* @return string
*/
protected function getContentPartNumber(): string
{
$partNumber = $this->getPartNumber();
if (0 === \count($this->getParts())) {
$partNumber .= '.1';
}
return $partNumber;
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
interface EmbeddedMessageInterface extends BasicMessageInterface
{
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
/**
* Collection of message headers.
*/
final class Headers extends Parameters
{
/**
* Constructor.
*
* @param \stdClass $headers
*/
public function __construct(\stdClass $headers)
{
parent::__construct();
// Store all headers as lowercase
$headers = \array_change_key_case((array) $headers);
foreach ($headers as $key => $value) {
$this[$key] = $this->parseHeader($key, $value);
}
}
/**
* Get header.
*
* @param string $key
*
* @return null|string
*/
public function get(string $key)
{
return parent::get(\strtolower($key));
}
/**
* Parse header.
*
* @param string $key
* @param mixed $value
*
* @return mixed
*/
private function parseHeader(string $key, $value)
{
switch ($key) {
case 'msgno':
return (int) $value;
case 'from':
case 'to':
case 'cc':
case 'bcc':
case 'reply_to':
case 'sender':
case 'return_path':
foreach ($value as $address) {
if (isset($address->mailbox)) {
$address->host = $address->host ?? null;
$address->personal = isset($address->personal) ? $this->decode($address->personal) : null;
}
}
return $value;
case 'date':
case 'subject':
return $this->decode($value);
}
return $value;
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
class Parameters extends \ArrayIterator
{
/**
* @var array
*/
private static $attachmentCustomKeys = [
'name*' => 'name',
'filename*' => 'filename',
];
/**
* @param array $parameters
*/
public function __construct(array $parameters = [])
{
parent::__construct();
$this->add($parameters);
}
/**
* @param array $parameters
*/
public function add(array $parameters = [])
{
foreach ($parameters as $parameter) {
$key = \strtolower($parameter->attribute);
if (isset(self::$attachmentCustomKeys[$key])) {
$key = self::$attachmentCustomKeys[$key];
}
$value = $this->decode($parameter->value);
$this[$key] = $value;
}
}
/**
* @param string $key
*
* @return mixed
*/
public function get(string $key)
{
return $this[$key] ?? null;
}
/**
* Decode value.
*
* @param string $value
*
* @return string
*/
final protected function decode(string $value): string
{
$parts = \imap_mime_header_decode($value);
if (!\is_array($parts)) {
return $value;
}
$decoded = '';
foreach ($parts as $part) {
$text = $part->text;
if ('default' !== $part->charset) {
$text = Transcoder::decode($text, $part->charset);
}
// RFC2231
if (1 === \preg_match('/^(?<encoding>[^\']+)\'[^\']*?\'(?<urltext>.+)$/', $text, $matches)) {
$hasInvalidChars = \preg_match('#[^%a-zA-Z0-9\-_\.\+]#', $matches['urltext']);
$hasEscapedChars = \preg_match('#%[a-zA-Z0-9]{2}#', $matches['urltext']);
if (!$hasInvalidChars && $hasEscapedChars) {
$text = Transcoder::decode(\urldecode($matches['urltext']), $matches['encoding']);
}
}
$decoded .= $text;
}
return $decoded;
}
}

View File

@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
/**
* A message part.
*/
interface PartInterface extends \RecursiveIterator
{
const TYPE_TEXT = 'text';
const TYPE_MULTIPART = 'multipart';
const TYPE_MESSAGE = 'message';
const TYPE_APPLICATION = 'application';
const TYPE_AUDIO = 'audio';
const TYPE_IMAGE = 'image';
const TYPE_VIDEO = 'video';
const TYPE_MODEL = 'model';
const TYPE_OTHER = 'other';
const TYPE_UNKNOWN = 'unknown';
const ENCODING_7BIT = '7bit';
const ENCODING_8BIT = '8bit';
const ENCODING_BINARY = 'binary';
const ENCODING_BASE64 = 'base64';
const ENCODING_QUOTED_PRINTABLE = 'quoted-printable';
const ENCODING_UNKNOWN = 'unknown';
const SUBTYPE_PLAIN = 'PLAIN';
const SUBTYPE_HTML = 'HTML';
/**
* Get message number (from headers).
*
* @return int
*/
public function getNumber(): int;
/**
* Part charset.
*
* @return string
*/
public function getCharset();
/**
* Part type.
*
* @return null|string
*/
public function getType();
/**
* Part subtype.
*
* @return null|string
*/
public function getSubtype();
/**
* Part encoding.
*
* @return null|string
*/
public function getEncoding();
/**
* Part disposition.
*
* @return null|string
*/
public function getDisposition();
/**
* Part bytes.
*
* @return null|string
*/
public function getBytes();
/**
* Part lines.
*
* @return null|string
*/
public function getLines();
/**
* Part parameters.
*
* @return Parameters
*/
public function getParameters(): Parameters;
/**
* Get raw part content.
*
* @return string
*/
public function getContent(): string;
/**
* Get decoded part content.
*
* @return string
*/
public function getDecodedContent(): string;
/**
* Part structure.
*
* @return \stdClass
*/
public function getStructure(): \stdClass;
/**
* Get part number.
*
* @return string
*/
public function getPartNumber(): string;
/**
* Get an array of all parts for this message.
*
* @return PartInterface[]
*/
public function getParts(): array;
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
/**
* A message part.
*/
final class SimplePart extends AbstractPart
{
}

View File

@@ -0,0 +1,326 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
use Ddeboer\Imap\Exception\UnsupportedCharsetException;
final class Transcoder
{
/**
* @var array
*
* @see https://encoding.spec.whatwg.org/#encodings
* @see https://dxr.mozilla.org/mozilla-central/source/dom/encoding/labelsencodings.properties
* @see https://dxr.mozilla.org/mozilla1.9.1/source/intl/uconv/src/charsetalias.properties
* @see https://msdn.microsoft.com/en-us/library/cc194829.aspx
*/
private static $charsetAliases = [
'128' => 'Shift_JIS',
'129' => 'EUC-KR',
'134' => 'GB2312',
'136' => 'Big5',
'161' => 'windows-1253',
'162' => 'windows-1254',
'177' => 'windows-1255',
'178' => 'windows-1256',
'186' => 'windows-1257',
'204' => 'windows-1251',
'222' => 'windows-874',
'238' => 'windows-1250',
'5601' => 'EUC-KR',
'646' => 'us-ascii',
'850' => 'IBM850',
'852' => 'IBM852',
'855' => 'IBM855',
'857' => 'IBM857',
'862' => 'IBM862',
'864' => 'IBM864',
'864i' => 'IBM864i',
'866' => 'IBM866',
'ansi-1251' => 'windows-1251',
'ansi_x3.4-1968' => 'us-ascii',
'arabic' => 'ISO-8859-6',
'ascii' => 'us-ascii',
'asmo-708' => 'ISO-8859-6',
'big5-hkscs' => 'Big5',
'chinese' => 'GB2312',
'cn-big5' => 'Big5',
'cns11643' => 'x-euc-tw',
'cp-866' => 'IBM866',
'cp1250' => 'windows-1250',
'cp1251' => 'windows-1251',
'cp1252' => 'windows-1252',
'cp1253' => 'windows-1253',
'cp1254' => 'windows-1254',
'cp1255' => 'windows-1255',
'cp1256' => 'windows-1256',
'cp1257' => 'windows-1257',
'cp1258' => 'windows-1258',
'cp819' => 'ISO-8859-1',
'cp850' => 'IBM850',
'cp852' => 'IBM852',
'cp855' => 'IBM855',
'cp857' => 'IBM857',
'cp862' => 'IBM862',
'cp864' => 'IBM864',
'cp864i' => 'IBM864i',
'cp866' => 'IBM866',
'cp932' => 'Shift_JIS',
'csbig5' => 'Big5',
'cseucjpkdfmtjapanese' => 'EUC-JP',
'cseuckr' => 'EUC-KR',
'cseucpkdfmtjapanese' => 'EUC-JP',
'csgb2312' => 'GB2312',
'csibm850' => 'IBM850',
'csibm852' => 'IBM852',
'csibm855' => 'IBM855',
'csibm857' => 'IBM857',
'csibm862' => 'IBM862',
'csibm864' => 'IBM864',
'csibm864i' => 'IBM864i',
'csibm866' => 'IBM866',
'csiso103t618bit' => 'T.61-8bit',
'csiso111ecmacyrillic' => 'ISO-IR-111',
'csiso2022jp' => 'ISO-2022-JP',
'csiso2022jp2' => 'ISO-2022-JP',
'csiso2022kr' => 'ISO-2022-KR',
'csiso58gb231280' => 'GB2312',
'csiso88596e' => 'ISO-8859-6-E',
'csiso88596i' => 'ISO-8859-6-I',
'csiso88598e' => 'ISO-8859-8-E',
'csiso88598i' => 'ISO-8859-8-I',
'csisolatin1' => 'ISO-8859-1',
'csisolatin2' => 'ISO-8859-2',
'csisolatin3' => 'ISO-8859-3',
'csisolatin4' => 'ISO-8859-4',
'csisolatin5' => 'ISO-8859-9',
'csisolatin6' => 'ISO-8859-10',
'csisolatin9' => 'ISO-8859-15',
'csisolatinarabic' => 'ISO-8859-6',
'csisolatincyrillic' => 'ISO-8859-5',
'csisolatingreek' => 'ISO-8859-7',
'csisolatinhebrew' => 'ISO-8859-8',
'cskoi8r' => 'KOI8-R',
'csksc56011987' => 'EUC-KR',
'csmacintosh' => 'x-mac-roman',
'csshiftjis' => 'Shift_JIS',
'csueckr' => 'EUC-KR',
'csunicode' => 'UTF-16BE',
'csunicode11' => 'UTF-16BE',
'csunicode11utf7' => 'UTF-7',
'csunicodeascii' => 'UTF-16BE',
'csunicodelatin1' => 'UTF-16BE',
'csviqr' => 'VIQR',
'csviscii' => 'VISCII',
'cyrillic' => 'ISO-8859-5',
'dos-874' => 'windows-874',
'ecma-114' => 'ISO-8859-6',
'ecma-118' => 'ISO-8859-7',
'ecma-cyrillic' => 'ISO-IR-111',
'elot_928' => 'ISO-8859-7',
'gb_2312' => 'GB2312',
'gb_2312-80' => 'GB2312',
'gbk' => 'x-gbk',
'greek' => 'ISO-8859-7',
'greek8' => 'ISO-8859-7',
'hebrew' => 'ISO-8859-8',
'ibm-864' => 'IBM864',
'ibm-864i' => 'IBM864i',
'ibm819' => 'ISO-8859-1',
'ibm874' => 'windows-874',
'iso-10646' => 'UTF-16BE',
'iso-10646-j-1' => 'UTF-16BE',
'iso-10646-ucs-2' => 'UTF-16BE',
'iso-10646-ucs-4' => 'UTF-32BE',
'iso-10646-ucs-basic' => 'UTF-16BE',
'iso-10646-unicode-latin1' => 'UTF-16BE',
'iso-2022-cn-ext' => 'ISO-2022-CN',
'iso-2022-jp-2' => 'ISO-2022-JP',
'iso-8859-8i' => 'ISO-8859-8-I',
'iso-ir-100' => 'ISO-8859-1',
'iso-ir-101' => 'ISO-8859-2',
'iso-ir-103' => 'T.61-8bit',
'iso-ir-109' => 'ISO-8859-3',
'iso-ir-110' => 'ISO-8859-4',
'iso-ir-126' => 'ISO-8859-7',
'iso-ir-127' => 'ISO-8859-6',
'iso-ir-138' => 'ISO-8859-8',
'iso-ir-144' => 'ISO-8859-5',
'iso-ir-148' => 'ISO-8859-9',
'iso-ir-149' => 'EUC-KR',
'iso-ir-157' => 'ISO-8859-10',
'iso-ir-58' => 'GB2312',
'iso8859-1' => 'ISO-8859-1',
'iso8859-10' => 'ISO-8859-10',
'iso8859-11' => 'ISO-8859-11',
'iso8859-13' => 'ISO-8859-13',
'iso8859-14' => 'ISO-8859-14',
'iso8859-15' => 'ISO-8859-15',
'iso8859-2' => 'ISO-8859-2',
'iso8859-3' => 'ISO-8859-3',
'iso8859-4' => 'ISO-8859-4',
'iso8859-5' => 'ISO-8859-5',
'iso8859-6' => 'ISO-8859-6',
'iso8859-7' => 'ISO-8859-7',
'iso8859-8' => 'ISO-8859-8',
'iso8859-9' => 'ISO-8859-9',
'iso88591' => 'ISO-8859-1',
'iso885910' => 'ISO-8859-10',
'iso885911' => 'ISO-8859-11',
'iso885912' => 'ISO-8859-12',
'iso885913' => 'ISO-8859-13',
'iso885914' => 'ISO-8859-14',
'iso885915' => 'ISO-8859-15',
'iso88592' => 'ISO-8859-2',
'iso88593' => 'ISO-8859-3',
'iso88594' => 'ISO-8859-4',
'iso88595' => 'ISO-8859-5',
'iso88596' => 'ISO-8859-6',
'iso88597' => 'ISO-8859-7',
'iso88598' => 'ISO-8859-8',
'iso88599' => 'ISO-8859-9',
'iso_8859-1' => 'ISO-8859-1',
'iso_8859-15' => 'ISO-8859-15',
'iso_8859-1:1987' => 'ISO-8859-1',
'iso_8859-2' => 'ISO-8859-2',
'iso_8859-2:1987' => 'ISO-8859-2',
'iso_8859-3' => 'ISO-8859-3',
'iso_8859-3:1988' => 'ISO-8859-3',
'iso_8859-4' => 'ISO-8859-4',
'iso_8859-4:1988' => 'ISO-8859-4',
'iso_8859-5' => 'ISO-8859-5',
'iso_8859-5:1988' => 'ISO-8859-5',
'iso_8859-6' => 'ISO-8859-6',
'iso_8859-6:1987' => 'ISO-8859-6',
'iso_8859-7' => 'ISO-8859-7',
'iso_8859-7:1987' => 'ISO-8859-7',
'iso_8859-8' => 'ISO-8859-8',
'iso_8859-8:1988' => 'ISO-8859-8',
'iso_8859-9' => 'ISO-8859-9',
'iso_8859-9:1989' => 'ISO-8859-9',
'koi' => 'KOI8-R',
'koi8' => 'KOI8-R',
'koi8-ru' => 'KOI8-U',
'koi8_r' => 'KOI8-R',
'korean' => 'EUC-KR',
'ks_c_5601-1987' => 'EUC-KR',
'ks_c_5601-1989' => 'EUC-KR',
'ksc5601' => 'EUC-KR',
'ksc_5601' => 'EUC-KR',
'l1' => 'ISO-8859-1',
'l2' => 'ISO-8859-2',
'l3' => 'ISO-8859-3',
'l4' => 'ISO-8859-4',
'l5' => 'ISO-8859-9',
'l6' => 'ISO-8859-10',
'l9' => 'ISO-8859-15',
'latin1' => 'ISO-8859-1',
'latin2' => 'ISO-8859-2',
'latin3' => 'ISO-8859-3',
'latin4' => 'ISO-8859-4',
'latin5' => 'ISO-8859-9',
'latin6' => 'ISO-8859-10',
'logical' => 'ISO-8859-8-I',
'mac' => 'x-mac-roman',
'macintosh' => 'x-mac-roman',
'ms932' => 'Shift_JIS',
'ms_kanji' => 'Shift_JIS',
'shift-jis' => 'Shift_JIS',
'sjis' => 'Shift_JIS',
'sun_eu_greek' => 'ISO-8859-7',
't.61' => 'T.61-8bit',
'tis620' => 'TIS-620',
'unicode-1-1-utf-7' => 'UTF-7',
'unicode-1-1-utf-8' => 'UTF-8',
'unicode-2-0-utf-7' => 'UTF-7',
'visual' => 'ISO-8859-8',
'windows-31j' => 'Shift_JIS',
'windows-949' => 'EUC-KR',
'x-cp1250' => 'windows-1250',
'x-cp1251' => 'windows-1251',
'x-cp1252' => 'windows-1252',
'x-cp1253' => 'windows-1253',
'x-cp1254' => 'windows-1254',
'x-cp1255' => 'windows-1255',
'x-cp1256' => 'windows-1256',
'x-cp1257' => 'windows-1257',
'x-cp1258' => 'windows-1258',
'x-euc-jp' => 'EUC-JP',
'x-iso-10646-ucs-2-be' => 'UTF-16BE',
'x-iso-10646-ucs-2-le' => 'UTF-16LE',
'x-iso-10646-ucs-4-be' => 'UTF-32BE',
'x-iso-10646-ucs-4-le' => 'UTF-32LE',
'x-sjis' => 'Shift_JIS',
'x-unicode-2-0-utf-7' => 'UTF-7',
'x-x-big5' => 'Big5',
'zh_cn.euc' => 'GB2312',
'zh_tw-big5' => 'Big5',
'zh_tw-euc' => 'x-euc-tw',
];
/**
* Decode text to UTF-8.
*
* @param string $text Text to decode
* @param string $fromCharset Original charset
*
* @return string
*/
public static function decode(string $text, string $fromCharset): string
{
static $utf8Aliases = [
'unicode-1-1-utf-8' => true,
'utf8' => true,
'utf-8' => true,
'UTF8' => true,
'UTF-8' => true,
];
if (isset($utf8Aliases[$fromCharset])) {
return $text;
}
$originalFromCharset = $fromCharset;
$lowercaseFromCharset = \strtolower($fromCharset);
if (isset(self::$charsetAliases[$lowercaseFromCharset])) {
$fromCharset = self::$charsetAliases[$lowercaseFromCharset];
}
\set_error_handler(function () {});
$iconvDecodedText = \iconv($fromCharset, 'UTF-8', $text);
if (false === $iconvDecodedText) {
$iconvDecodedText = \iconv($originalFromCharset, 'UTF-8', $text);
}
\restore_error_handler();
if (false !== $iconvDecodedText) {
return $iconvDecodedText;
}
$errorMessage = null;
$errorNumber = 0;
\set_error_handler(function ($nr, $message) use (&$errorMessage, &$errorNumber) {
$errorMessage = $message;
$errorNumber = $nr;
});
$decodedText = \mb_convert_encoding($text, 'UTF-8', $fromCharset);
\restore_error_handler();
if (null !== $errorMessage) {
throw new UnsupportedCharsetException(\sprintf(
'Unsupported charset "%s"%s: %s',
$originalFromCharset,
($fromCharset !== $originalFromCharset) ? \sprintf(' (alias found: "%s")', $fromCharset) : '',
$errorMessage
), $errorNumber);
}
return $decodedText;
}
}

View File

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
/**
* An IMAP message (e-mail).
*/
interface MessageInterface extends Message\BasicMessageInterface
{
/**
* Get raw part content.
*
* @return string
*/
public function getContent(): string;
/**
* Get message recent flag value (from headers).
*
* @return null|string
*/
public function isRecent();
/**
* Get message unseen flag value (from headers).
*
* @return bool
*/
public function isUnseen(): bool;
/**
* Get message flagged flag value (from headers).
*
* @return bool
*/
public function isFlagged(): bool;
/**
* Get message answered flag value (from headers).
*
* @return bool
*/
public function isAnswered(): bool;
/**
* Get message deleted flag value (from headers).
*
* @return bool
*/
public function isDeleted(): bool;
/**
* Get message draft flag value (from headers).
*
* @return bool
*/
public function isDraft(): bool;
/**
* Has the message been marked as read?
*
* @return bool
*/
public function isSeen(): bool;
/**
* Mark message as seen.
*
* @return bool
*
* @deprecated since version 1.1, to be removed in 2.0
*/
public function maskAsSeen(): bool;
/**
* Mark message as seen.
*
* @return bool
*/
public function markAsSeen(): bool;
/**
* Move message to another mailbox.
*
* @param MailboxInterface $mailbox
*/
public function copy(MailboxInterface $mailbox);
/**
* Move message to another mailbox.
*
* @param MailboxInterface $mailbox
*/
public function move(MailboxInterface $mailbox);
/**
* Delete message.
*/
public function delete();
/**
* Set Flag Message.
*
* @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
*
* @return bool
*/
public function setFlag(string $flag): bool;
/**
* Clear Flag Message.
*
* @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
*
* @return bool
*/
public function clearFlag(string $flag): bool;
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
final class MessageIterator extends \ArrayIterator implements MessageIteratorInterface
{
/**
* @var ImapResourceInterface
*/
private $resource;
/**
* Constructor.
*
* @param ImapResourceInterface $resource IMAP resource
* @param array $messageNumbers Array of message numbers
*/
public function __construct(ImapResourceInterface $resource, array $messageNumbers)
{
$this->resource = $resource;
parent::__construct($messageNumbers);
}
/**
* Get current message.
*
* @return MessageInterface
*/
public function current(): MessageInterface
{
$current = parent::current();
if (!\is_int($current)) {
throw new Exception\OutOfBoundsException(\sprintf(
'The current value "%s" isn\'t an integer and doesn\'t represent a message;'
. ' try to cycle this "%s" with a native php function like foreach or with the method getArrayCopy(),'
. ' or check it by calling the methods valid().',
\is_object($current) ? \get_class($current) : \gettype($current),
static::class
));
}
return new Message($this->resource, $current);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
interface MessageIteratorInterface extends \Iterator
{
/**
* Get current message.
*
* @return MessageInterface
*/
public function current(): MessageInterface;
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search;
use DateTimeInterface;
/**
* Represents a date condition.
*/
abstract class AbstractDate implements ConditionInterface
{
/**
* Format for dates to be sent to the IMAP server.
*
* @var string
*/
private $dateFormat;
/**
* The date to be used for the condition.
*
* @var DateTimeInterface
*/
private $date;
/**
* Constructor.
*
* @param DateTimeInterface $date optional date for the condition
*/
public function __construct(DateTimeInterface $date, string $dateFormat = 'j-M-Y')
{
$this->date = $date;
$this->dateFormat = $dateFormat;
}
/**
* Converts the condition to a string that can be sent to the IMAP server.
*
* @return string
*/
final public function toString(): string
{
return \sprintf('%s "%s"', $this->getKeyword(), $this->date->format($this->dateFormat));
}
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
abstract protected function getKeyword(): string;
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search;
/**
* Represents a text based condition. Text based conditions use a contains
* restriction.
*/
abstract class AbstractText implements ConditionInterface
{
/**
* Text to be used for the condition.
*
* @var string
*/
private $text;
/**
* Constructor.
*
* @param string $text optional text for the condition
*/
public function __construct(string $text)
{
$this->text = $text;
}
/**
* Converts the condition to a string that can be sent to the IMAP server.
*
* @return string
*/
final public function toString(): string
{
return \sprintf('%s "%s"', $this->getKeyword(), $this->text);
}
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
abstract protected function getKeyword(): string;
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search;
/**
* Represents a condition that can be used in a search expression.
*/
interface ConditionInterface
{
/**
* Converts the condition to a string that can be sent to the IMAP server.
*
* @return string
*/
public function toString(): string;
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Date;
use Ddeboer\Imap\Search\AbstractDate;
/**
* Represents a date before condition. Messages must have a date before the
* specified date in order to match the condition.
*/
final class Before extends AbstractDate
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'BEFORE';
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Date;
use Ddeboer\Imap\Search\AbstractDate;
/**
* Represents a date on condition. Messages must have a date matching the
* specified date in order to match the condition.
*/
final class On extends AbstractDate
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'ON';
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Date;
use Ddeboer\Imap\Search\AbstractDate;
/**
* Represents a date after condition. Messages must have a date after the
* specified date in order to match the condition.
*/
final class Since extends AbstractDate
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'SINCE';
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Email;
use Ddeboer\Imap\Search\AbstractText;
/**
* Represents a "Bcc" email address condition. Messages must have been addressed
* to the specified recipient (along with any others) in order to match the
* condition.
*/
final class Bcc extends AbstractText
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'BCC';
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Email;
use Ddeboer\Imap\Search\AbstractText;
/**
* Represents a "Cc" email address condition. Messages must have been addressed
* to the specified recipient (along with any others) in order to match the
* condition.
*/
final class Cc extends AbstractText
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'CC';
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Email;
use Ddeboer\Imap\Search\AbstractText;
/**
* Represents a "From" email address condition. Messages must have been sent
* from the specified email address in order to match the condition.
*/
final class From extends AbstractText
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'FROM';
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Email;
use Ddeboer\Imap\Search\AbstractText;
/**
* Represents a "To" email address condition. Messages must have been addressed
* to the specified recipient (along with any others) in order to match the
* condition.
*/
final class To extends AbstractText
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'TO';
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Flag;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents an ANSWERED flag condition. Messages must have the \\ANSWERED flag
* set in order to match the condition.
*/
final class Answered implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'ANSWERED';
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Flag;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents a FLAGGED flag condition. Messages must have the \\FLAGGED flag
* (i.e. urgent or important) set in order to match the condition.
*/
final class Flagged implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'FLAGGED';
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Flag;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents an RECENT flag condition. Messages must have the \\RECENT flag
* set in order to match the condition.
*/
final class Recent implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'RECENT';
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Flag;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents an SEEN flag condition. Messages must have the \\SEEN flag
* set in order to match the condition.
*/
final class Seen implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'SEEN';
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Flag;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents an UNANSWERED flag condition. Messages must not have the
* \\ANSWERED flag set in order to match the condition.
*/
final class Unanswered implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'UNANSWERED';
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Flag;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents a UNFLAGGED flag condition. Messages must no have the \\FLAGGED
* flag (i.e. urgent or important) set in order to match the condition.
*/
final class Unflagged implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'UNFLAGGED';
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Flag;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents an UNSEEN flag condition. Messages must not have the \\SEEN flag
* set in order to match the condition.
*/
final class Unseen implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'UNSEEN';
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\LogicalOperator;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents an ALL operator. Messages must match all conditions following this
* operator in order to match the expression.
*/
final class All implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'ALL';
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\LogicalOperator;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents an OR operator. Messages only need to match one of the conditions
* after this operator to match the expression.
*/
final class OrConditions implements ConditionInterface
{
/**
* The conditions that together represent the expression.
*
* @var array
*/
private $conditions = [];
public function __construct(array $conditions)
{
foreach ($conditions as $condition) {
$this->addCondition($condition);
}
}
/**
* Adds a new condition to the expression.
*
* @param ConditionInterface $condition the condition to be added
*/
private function addCondition(ConditionInterface $condition)
{
$this->conditions[] = $condition;
}
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
$conditions = \array_map(function (ConditionInterface $condition) {
return $condition->toString();
}, $this->conditions);
return \sprintf('( %s )', \implode(' OR ', $conditions));
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search;
/**
* Represents a raw expression.
*/
final class RawExpression implements ConditionInterface
{
/**
* Text to be used for the condition.
*
* @var string
*/
private $expression;
/**
* @param string $expression text for the condition
*/
public function __construct(string $expression)
{
$this->expression = $expression;
}
/**
* @return string
*/
public function toString(): string
{
return $this->expression;
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\State;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents a DELETED condition. Messages must have been marked for deletion
* but not yet expunged in order to match the condition.
*/
final class Deleted implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'DELETED';
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\State;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents a NEW condition. Only new messages will match this condition.
*/
final class NewMessage implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'NEW';
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\State;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents an OLD condition. Only old messages will match this condition.
*/
final class Old implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'OLD';
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\State;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents a UNDELETED condition. Messages must not have been marked for
* deletion in order to match the condition.
*/
final class Undeleted implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'UNDELETED';
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Text;
use Ddeboer\Imap\Search\AbstractText;
/**
* Represents a body text contains condition. Messages must have a body
* containing the specified text in order to match the condition.
*/
final class Body extends AbstractText
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'BODY';
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Text;
use Ddeboer\Imap\Search\AbstractText;
/**
* Represents a keyword text contains condition. Messages must have a keyword
* matching the specified text in order to match the condition.
*/
final class Keyword extends AbstractText
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'KEYWORD';
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Text;
use Ddeboer\Imap\Search\AbstractText;
/**
* Represents a subject contains condition. Messages must have a subject
* containing the specified text in order to match the condition.
*/
final class Subject extends AbstractText
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'SUBJECT';
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Text;
use Ddeboer\Imap\Search\AbstractText;
/**
* Represents a message text contains condition. Messages must contain the
* specified text in order to match the condition.
*/
final class Text extends AbstractText
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'TEXT';
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Text;
use Ddeboer\Imap\Search\AbstractText;
/**
* Represents a keyword text does not contain condition. Messages must not have
* a keyword matching the specified text in order to match the condition.
*/
final class Unkeyword extends AbstractText
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'UNKEYWORD';
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Defines a search expression that can be used to look up email messages.
*/
final class SearchExpression implements ConditionInterface
{
/**
* The conditions that together represent the expression.
*
* @var array
*/
private $conditions = [];
/**
* Adds a new condition to the expression.
*
* @param ConditionInterface $condition the condition to be added
*
* @return self
*/
public function addCondition(ConditionInterface $condition): self
{
$this->conditions[] = $condition;
return $this;
}
/**
* Converts the expression to a string that can be sent to the IMAP server.
*
* @return string
*/
public function toString(): string
{
$conditions = \array_map(function (ConditionInterface $condition) {
return $condition->toString();
}, $this->conditions);
return \implode(' ', $conditions);
}
}

View File

@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use Ddeboer\Imap\Exception\AuthenticationFailedException;
/**
* An IMAP server.
*/
final class Server implements ServerInterface
{
/**
* @var string Internet domain name or bracketed IP address of server
*/
private $hostname;
/**
* @var string TCP port number
*/
private $port;
/**
* @var string Optional flags
*/
private $flags;
/**
* @var array
*/
private $parameters;
/**
* Constructor.
*
* @param string $hostname Internet domain name or bracketed IP address
* of server
* @param string $port TCP port number
* @param string $flags Optional flags
* @param array $parameters Connection parameters
*/
public function __construct(
string $hostname,
string $port = '993',
string $flags = '/imap/ssl/validate-cert',
array $parameters = []
) {
if (!\function_exists('imap_open')) {
throw new \RuntimeException('IMAP extension must be enabled');
}
$this->hostname = $hostname;
$this->port = $port;
$this->flags = $flags ? '/' . \ltrim($flags, '/') : '';
$this->parameters = $parameters;
}
/**
* Authenticate connection.
*
* @param string $username Username
* @param string $password Password
*
* @throws AuthenticationFailedException
*
* @return ConnectionInterface
*/
public function authenticate(string $username, string $password): ConnectionInterface
{
$errorMessage = null;
$errorNumber = 0;
\set_error_handler(function ($nr, $message) use (&$errorMessage, &$errorNumber) {
$errorMessage = $message;
$errorNumber = $nr;
});
$resource = \imap_open(
$this->getServerString(),
$username,
$password,
0,
1,
$this->parameters
);
\restore_error_handler();
if (false === $resource || null !== $errorMessage) {
throw new AuthenticationFailedException(\sprintf(
'Authentication failed for user "%s"%s',
$username,
null !== $errorMessage ? ': ' . $errorMessage : ''
), $errorNumber);
}
$check = \imap_check($resource);
$mailbox = $check->Mailbox;
$connection = \substr($mailbox, 0, \strpos($mailbox, '}') + 1);
// These are necessary to get rid of PHP throwing IMAP errors
\imap_errors();
\imap_alerts();
return new Connection(new ImapResource($resource), $connection);
}
/**
* Glues hostname, port and flags and returns result.
*
* @return string
*/
private function getServerString(): string
{
return \sprintf(
'{%s%s%s}',
$this->hostname,
'' !== $this->port ? ':' . $this->port : '',
$this->flags
);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
/**
* An IMAP server.
*/
interface ServerInterface
{
/**
* Authenticate connection.
*
* @param string $username Username
* @param string $password Password
*
* @return ConnectionInterface
*/
public function authenticate(string $username, string $password): ConnectionInterface;
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Test;
use Ddeboer\Imap\MessageInterface;
use Ddeboer\Imap\MessageIteratorInterface;
/**
* A MessageIterator to be used in a mocked environment.
*/
final class RawMessageIterator extends \ArrayIterator implements MessageIteratorInterface
{
public function current(): MessageInterface
{
return parent::current();
}
}