This commit is contained in:
traveler 2025-04-17 17:29:05 -05:00
commit 5aa7d034f7
3292 changed files with 465160 additions and 0 deletions

View file

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
return (new PhpCsFixer\Config())
->setRiskyAllowed(true)
->setRules([
'@DoctrineAnnotation' => true,
'@Symfony' => true,
'@Symfony:risky' => true,
'@PHPUnit75Migration:risky' => true,
'@PHP71Migration' => true,
'@PHP70Migration:risky' => true, // @TODO with next major version
'align_multiline_comment' => ['comment_type' => 'all_multiline'],
'array_indentation' => true,
'array_syntax' => ['syntax' => 'short'],
'binary_operator_spaces' => ['default' => 'align_single_space'],
'blank_line_before_statement' => true,
'class_definition' => ['single_item_single_line' => true],
'compact_nullable_typehint' => true,
'concat_space' => ['spacing' => 'one'],
'echo_tag_syntax' => ['format' => 'long'],
'error_suppression' => false,
'escape_implicit_backslashes' => true,
'explicit_indirect_variable' => true,
'explicit_string_variable' => true,
'fully_qualified_strict_types' => true,
'heredoc_to_nowdoc' => true,
'list_syntax' => ['syntax' => 'long'],
'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
'method_chaining_indentation' => true,
'multiline_comment_opening_closing' => true,
'multiline_whitespace_before_semicolons' => ['strategy' => 'new_line_for_chained_calls'],
'native_constant_invocation' => true,
'native_function_invocation' => ['include' => ['@internal']],
'no_alternative_syntax' => true,
'no_break_comment' => true,
'no_extra_blank_lines' => ['tokens' => ['break', 'continue', 'extra', 'return', 'throw', 'use', 'parenthesis_brace_block', 'square_brace_block', 'curly_brace_block']],
'no_null_property_initialization' => true,
'no_php4_constructor' => true,
'no_superfluous_elseif' => true,
'no_unneeded_curly_braces' => true,
'no_unneeded_final_method' => true,
'no_unreachable_default_argument_value' => true,
'no_useless_else' => true,
'no_useless_return' => true,
'ordered_imports' => true,
'php_unit_method_casing' => true,
'php_unit_set_up_tear_down_visibility' => true,
'php_unit_strict' => true,
'php_unit_test_annotation' => true,
'php_unit_test_case_static_method_calls' => true,
'php_unit_test_class_requires_covers' => false,
'phpdoc_add_missing_param_annotation' => true,
'phpdoc_order' => true,
'phpdoc_order_by_value' => true,
'phpdoc_types_order' => true,
'random_api_migration' => true,
'semicolon_after_instruction' => true,
'simplified_null_return' => true,
'single_line_comment_style' => true,
'single_line_throw' => false,
'space_after_semicolon' => true,
'static_lambda' => true,
'strict_comparison' => true,
'string_line_ending' => true,
])
->setFinder(
PhpCsFixer\Finder::create()
->in(__DIR__ . '/src')
->in(__DIR__ . '/tests')
)
;

19
data/web/inc/lib/vendor/ddeboer/imap/LICENSE vendored Executable file
View file

@ -0,0 +1,19 @@
Copyright (C) 2013 David de Boer <david@ddeboer.nl>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

307
data/web/inc/lib/vendor/ddeboer/imap/README.md vendored Executable file
View file

@ -0,0 +1,307 @@
# PHP IMAP library
[![Latest Stable Version](https://img.shields.io/packagist/v/ddeboer/imap.svg)](https://packagist.org/packages/ddeboer/imap)
[![Downloads](https://img.shields.io/packagist/dt/ddeboer/imap.svg)](https://packagist.org/packages/ddeboer/imap)
[![Integrate](https://github.com/ddeboer/imap/workflows/Integrate/badge.svg?branch=master)](https://github.com/ddeboer/imap/actions)
[![Code Coverage](https://codecov.io/gh/ddeboer/imap/coverage.svg?branch=master)](https://codecov.io/gh/ddeboer/imap?branch=master)
A PHP IMAP library to read and process e-mails over IMAP protocol, built with robust Object-Oriented architecture.
This library requires PHP >= 7.4 with [IMAP](https://www.php.net/manual/en/book.imap.php),
[iconv](https://www.php.net/manual/en/book.iconv.php) and
[Multibyte String](https://www.php.net/manual/en/book.mbstring.php) extensions installed.
## Installation
The recommended way to install the IMAP library is through [Composer](https://getcomposer.org):
```bash
$ composer require ddeboer/imap
```
This command requires you to have Composer installed globally, as explained
in the [installation chapter](https://getcomposer.org/doc/00-intro.md)
of the Composer documentation.
## Usage
### Connect and Authenticate
```php
use Ddeboer\Imap\Server;
$server = new Server('imap.gmail.com');
// $connection is instance of \Ddeboer\Imap\Connection
$connection = $server->authenticate('my_username', 'my_password');
```
You can specify port, [flags and parameters](https://secure.php.net/manual/en/function.imap-open.php)
to the server:
```php
$server = new Server(
$hostname, // required
$port, // defaults to '993'
$flags, // defaults to '/imap/ssl/validate-cert'
$parameters
);
```
### Mailboxes
Retrieve mailboxes (also known as mail folders) from the mail server and iterate
over them:
```php
$mailboxes = $connection->getMailboxes();
foreach ($mailboxes as $mailbox) {
// Skip container-only mailboxes
// @see https://secure.php.net/manual/en/function.imap-getmailboxes.php
if ($mailbox->getAttributes() & \LATT_NOSELECT) {
continue;
}
// $mailbox is instance of \Ddeboer\Imap\Mailbox
printf('Mailbox "%s" has %s messages', $mailbox->getName(), $mailbox->count());
}
```
Or retrieve a specific mailbox:
```php
$mailbox = $connection->getMailbox('INBOX');
```
Delete a mailbox:
```php
$connection->deleteMailbox($mailbox);
```
You can bulk set, or clear, any [flag](https://secure.php.net/manual/en/function.imap-setflag-full.php) of mailbox messages (by UIDs):
```php
$mailbox->setFlag('\\Seen \\Flagged', ['1:5', '7', '9']);
$mailbox->setFlag('\\Seen', '1,3,5,6:8');
$mailbox->clearFlag('\\Flagged', '1,3');
```
**WARNING** You must retrieve new Message instances in case of bulk modify flags to refresh the single Messages flags.
### Messages
Retrieve messages (e-mails) from a mailbox and iterate over them:
```php
$messages = $mailbox->getMessages();
foreach ($messages as $message) {
// $message is instance of \Ddeboer\Imap\Message
}
```
To insert a new message (that just has been sent) into the Sent mailbox and flag it as seen:
```php
$mailbox = $connection->getMailbox('Sent');
$mailbox->addMessage($messageMIME, '\\Seen');
```
Note that the message should be a string at MIME format (as described in the [RFC2045](https://tools.ietf.org/html/rfc2045)).
#### Searching for Messages
```php
use Ddeboer\Imap\SearchExpression;
use Ddeboer\Imap\Search\Email\To;
use Ddeboer\Imap\Search\Text\Body;
$search = new SearchExpression();
$search->addCondition(new To('me@here.com'));
$search->addCondition(new Body('contents'));
$messages = $mailbox->getMessages($search);
```
**WARNING** We are currently unable to have both spaces _and_ double-quotes
escaped together. Only spaces are currently escaped correctly.
You can use `Ddeboer\Imap\Search\RawExpression` to write the complete search
condition by yourself.
Messages can also be retrieved sorted as per [imap_sort](https://secure.php.net/manual/en/function.imap-sort.php)
function:
```php
$today = new DateTimeImmutable();
$thirtyDaysAgo = $today->sub(new DateInterval('P30D'));
$messages = $mailbox->getMessages(
new Ddeboer\Imap\Search\Date\Since($thirtyDaysAgo),
\SORTDATE, // Sort criteria
true // Descending order
);
```
#### Unknown search criterion: OR
Note that PHP imap library relies on the `c-client` library available at https://www.washington.edu/imap/
which doesn't fully support some IMAP4 search criteria like `OR`. If you want those unsupported criteria,
you need to manually patch the latest version (`imap-2007f` of 23-Jul-2011 at the time of this commit)
and recompile PHP onto your patched `c-client` library.
By the way most of the common search criteria are available and functioning, browse them in `./src/Search`.
References:
1. https://stackoverflow.com/questions/36356715/imap-search-unknown-search-criterion-or
1. imap-2007f.tar.gz: `./src/c-client/mail.c` and `./docs/internal.txt`
#### Message Properties and Operations
Get message number and unique [message id](https://en.wikipedia.org/wiki/Message-ID)
in the form <...>:
```php
$message->getNumber();
$message->getId();
```
Get other message properties:
```php
$message->getSubject();
$message->getFrom(); // Message\EmailAddress
$message->getTo(); // array of Message\EmailAddress
$message->getDate(); // DateTimeImmutable
$message->isAnswered();
$message->isDeleted();
$message->isDraft();
$message->isSeen();
```
Get message headers as a [\Ddeboer\Imap\Message\Headers](/src/Ddeboer/Imap/Message/Headers.php) object:
```php
$message->getHeaders();
```
Get message body as HTML or plain text:
```php
$message->getBodyHtml(); // Content of text/html part, if present
$message->getBodyText(); // Content of text/plain part, if present
```
Reading the message body keeps the message as unseen.
If you want to mark the message as seen:
```php
$message->markAsSeen();
```
Or you can set, or clear, any [flag](https://secure.php.net/manual/en/function.imap-setflag-full.php):
```php
$message->setFlag('\\Seen \\Flagged');
$message->clearFlag('\\Flagged');
```
Move a message to another mailbox:
```php
$mailbox = $connection->getMailbox('another-mailbox');
$message->move($mailbox);
```
Deleting messages:
```php
$mailbox->getMessage(1)->delete();
$mailbox->getMessage(2)->delete();
$connection->expunge();
```
### Message Attachments
Get message attachments (both inline and attached) and iterate over them:
```php
$attachments = $message->getAttachments();
foreach ($attachments as $attachment) {
// $attachment is instance of \Ddeboer\Imap\Message\Attachment
}
```
Download a message attachment to a local file:
```php
// getDecodedContent() decodes the attachments contents automatically:
file_put_contents(
'/my/local/dir/' . $attachment->getFilename(),
$attachment->getDecodedContent()
);
```
### Embedded Messages
Check if attachment is embedded message and get it:
```php
$attachments = $message->getAttachments();
foreach ($attachments as $attachment) {
if ($attachment->isEmbeddedMessage()) {
$embeddedMessage = $attachment->getEmbeddedMessage();
// $embeddedMessage is instance of \Ddeboer\Imap\Message\EmbeddedMessage
}
}
```
An EmbeddedMessage has the same API as a normal Message, apart from flags
and operations like copy, move or delete.
### Timeouts
The IMAP extension provides the [imap_timeout](https://secure.php.net/manual/en/function.imap-timeout.php)
function to adjust the timeout seconds for various operations.
However the extension's implementation doesn't link the functionality to a
specific context or connection, instead they are global. So in order to not
affect functionalities outside this library, we had to choose whether wrap
every `imap_*` call around an optional user-provided timeout or leave this
task to the user.
Because of the heterogeneous world of IMAP servers and the high complexity
burden cost for such a little gain of the former, we chose the latter.
## Mock the library
Mockability is granted by interfaces present for each API.
Dig into [MockabilityTest](tests/MockabilityTest.php) for an example of a
mocked workflow.
## Running the Tests
This library is functionally tested on [Travis CI](https://travis-ci.org/ddeboer/imap)
against a local Dovecot server.
If you have your own IMAP (test) account, you can run the tests locally by
providing your IMAP credentials:
```bash
$ composer install
$ IMAP_SERVER_NAME="my.imap.server.com" IMAP_SERVER_PORT="60993" IMAP_USERNAME="johndoe" IMAP_PASSWORD="p4ssword" vendor/bin/phpunit
```
You can also copy `phpunit.xml.dist` file to a custom `phpunit.xml` and put
these environment variables in it.
**WARNING** Tests create new mailboxes without removing them.
```
$ docker-compose run tests
```

View file

@ -0,0 +1,54 @@
{
"name": "ddeboer/imap",
"description": "Object-oriented IMAP for PHP",
"license": "MIT",
"keywords": [
"email",
"mail",
"imap"
],
"authors": [
{
"name": "David de Boer",
"email": "david@ddeboer.nl"
},
{
"name": "Filippo Tessarotto",
"email": "zoeslam@gmail.com"
},
{
"name": "Community contributors",
"homepage": "https://github.com/ddeboer/imap/graphs/contributors"
}
],
"require": {
"php": "^8.0.1",
"ext-iconv": "*",
"ext-imap": "*",
"ext-mbstring": "*"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^v3.4.0",
"laminas/laminas-mail": "^2.15.1",
"malukenho/mcbumpface": "^1.1.5",
"phpstan/phpstan": "^1.3.3",
"phpstan/phpstan-phpunit": "^1.0.0",
"phpstan/phpstan-strict-rules": "^1.1.0",
"phpunit/phpunit": "^9.5.11"
},
"autoload": {
"psr-4": {
"Ddeboer\\Imap\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Ddeboer\\Imap\\Tests\\": "tests/"
}
},
"config": {
"allow-plugins": {
"malukenho/mcbumpface": true
}
}
}

View file

@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use Ddeboer\Imap\Exception\CreateMailboxException;
use Ddeboer\Imap\Exception\DeleteMailboxException;
use Ddeboer\Imap\Exception\ImapGetmailboxesException;
use Ddeboer\Imap\Exception\ImapNumMsgException;
use Ddeboer\Imap\Exception\ImapQuotaException;
use Ddeboer\Imap\Exception\MailboxDoesNotExistException;
/**
* A connection to an IMAP server that is authenticated for a user.
*/
final class Connection implements ConnectionInterface
{
private ImapResourceInterface $resource;
private string $server;
/**
* @var null|MailboxInterface[]
*/
private ?array $mailboxes = null;
/**
* @var null|array<int|string, \stdClass>
*/
private ?array $mailboxNames = null;
/**
* Constructor.
*
* @throws \InvalidArgumentException
*/
public function __construct(ImapResourceInterface $resource, string $server)
{
$this->resource = $resource;
$this->server = $server;
}
public function getResource(): ImapResourceInterface
{
return $this->resource;
}
public function expunge(): bool
{
return \imap_expunge($this->resource->getStream());
}
public function close(int $flag = 0): bool
{
$this->resource->clearLastMailboxUsedCache();
return \imap_close($this->resource->getStream(), $flag);
}
public function getQuota(string $root = 'INBOX'): array
{
$errorMessage = null;
$errorNumber = 0;
\set_error_handler(static function ($nr, $message) use (&$errorMessage, &$errorNumber): bool {
$errorMessage = $message;
$errorNumber = $nr;
return true;
});
$return = \imap_get_quotaroot($this->resource->getStream(), $root);
\restore_error_handler();
if (false === $return || null !== $errorMessage) {
throw new ImapQuotaException(
\sprintf(
'IMAP Quota request failed for "%s"%s',
$root,
null !== $errorMessage ? ': ' . $errorMessage : ''
),
$errorNumber
);
}
return $return;
}
public function getMailboxes(): array
{
$this->initMailboxNames();
\assert(null !== $this->mailboxNames);
if (null === $this->mailboxes) {
$this->mailboxes = [];
foreach ($this->mailboxNames as $mailboxName => $mailboxInfo) {
$this->mailboxes[(string) $mailboxName] = $this->getMailbox((string) $mailboxName);
}
}
return $this->mailboxes;
}
public function hasMailbox(string $name): bool
{
$this->initMailboxNames();
\assert(null !== $this->mailboxNames);
return isset($this->mailboxNames[$name]);
}
public function getMailbox(string $name): MailboxInterface
{
if (false === $this->hasMailbox($name)) {
throw new MailboxDoesNotExistException(\sprintf('Mailbox name "%s" does not exist', $name));
}
\assert(isset($this->mailboxNames[$name]));
return new Mailbox($this->resource, $name, $this->mailboxNames[$name]);
}
#[\ReturnTypeWillChange]
public function count()
{
$return = \imap_num_msg($this->resource->getStream());
if (false === $return) {
throw new ImapNumMsgException('imap_num_msg failed');
}
return $return;
}
public function ping(): bool
{
return \imap_ping($this->resource->getStream());
}
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);
}
public function deleteMailbox(MailboxInterface $mailbox): void
{
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();
}
private function initMailboxNames(): void
{
if (null !== $this->mailboxNames) {
return;
}
$this->mailboxNames = [];
$mailboxesInfo = \imap_getmailboxes($this->resource->getStream(), $this->server, '*');
if (!\is_array($mailboxesInfo)) {
throw new ImapGetmailboxesException('imap_getmailboxes failed');
}
foreach ($mailboxesInfo as $mailboxInfo) {
$name = \mb_convert_encoding(\str_replace($this->server, '', $mailboxInfo->name), 'UTF-8', 'UTF7-IMAP');
\assert(\is_string($name));
$this->mailboxNames[$name] = $mailboxInfo;
}
}
}

View file

@ -0,0 +1,82 @@
<?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.
*/
interface ConnectionInterface extends \Countable
{
/**
* Get IMAP resource.
*/
public function getResource(): ImapResourceInterface;
/**
* Delete all messages marked for deletion.
*/
public function expunge(): bool;
/**
* Close connection.
*/
public function close(int $flag = 0): bool;
/**
* Check if the connection is still active.
*
* @throws InvalidResourceException If connection was closed
*/
public function ping(): bool;
/**
* Get Mailbox quota.
*
* @return array<string, int>
*/
public function getQuota(string $root = 'INBOX'): array;
/**
* 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
*/
public function hasMailbox(string $name): bool;
/**
* Get a mailbox by its name.
*
* @param string $name Mailbox name
*
* @throws MailboxDoesNotExistException If mailbox does not exist
*/
public function getMailbox(string $name): MailboxInterface;
/**
* Create mailbox.
*
* @throws CreateMailboxException
*/
public function createMailbox(string $name): MailboxInterface;
/**
* Delete mailbox.
*
* @throws DeleteMailboxException
*/
public function deleteMailbox(MailboxInterface $mailbox): void;
}

View file

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
abstract class AbstractException extends \RuntimeException
{
private const ERROR_LABELS = [
\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',
];
final public function __construct(string $message, int $code = 0, \Throwable $previous = null)
{
$errorType = '';
if (isset(self::ERROR_LABELS[$code])) {
$errorType = \sprintf('[%s] ', self::ERROR_LABELS[$code]);
}
$joinString = "\n- ";
$alerts = \imap_alerts();
$errors = \imap_errors();
$completeMessage = \sprintf(
"%s%s\nimap_alerts (%s):%s\nimap_errors (%s):%s",
$errorType,
$message,
false !== $alerts ? \count($alerts) : 0,
false !== $alerts ? $joinString . \implode($joinString, $alerts) : '',
false !== $errors ? \count($errors) : 0,
false !== $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 ImapFetchbodyException extends AbstractException
{
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class ImapStatusException 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 MessageUndeleteException 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 ResourceCheckFailureException 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,99 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use Ddeboer\Imap\Exception\InvalidResourceException;
use Ddeboer\Imap\Exception\ReopenMailboxException;
use IMAP\Connection;
/**
* An imap resource stream.
*/
final class ImapResource implements ImapResourceInterface
{
/**
* @var resource
*/
private $resource;
private ?MailboxInterface $mailbox = null;
private static ?string $lastMailboxUsedCache = null;
/**
* Constructor.
*
* @param Connection|resource $resource
*/
public function __construct($resource, MailboxInterface $mailbox = null)
{
$this->resource = $resource;
$this->mailbox = $mailbox;
}
public function getStream()
{
if (
!$this->resource instanceof Connection
&& (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;
}
public function clearLastMailboxUsedCache(): void
{
self::$lastMailboxUsedCache = null;
}
/**
* If connection is not currently in this mailbox, switch it to this mailbox.
*/
private function initMailbox(): void
{
if (null === $this->mailbox || self::isMailboxOpen($this->mailbox, $this->resource)) {
return;
}
\set_error_handler(static function (): bool {
return true;
});
\imap_reopen($this->resource, $this->mailbox->getFullEncodedName());
\restore_error_handler();
if (self::isMailboxOpen($this->mailbox, $this->resource)) {
return;
}
throw new ReopenMailboxException(\sprintf('Cannot reopen mailbox "%s"', $this->mailbox->getName()));
}
/**
* Check whether the current mailbox is open.
*
* @param resource $resource
*/
private static function isMailboxOpen(MailboxInterface $mailbox, $resource): bool
{
$currentMailboxName = $mailbox->getFullEncodedName();
if ($currentMailboxName === self::$lastMailboxUsedCache) {
return true;
}
self::$lastMailboxUsedCache = null;
$check = \imap_check($resource);
$return = false !== $check && $check->Mailbox === $currentMailboxName;
if (true === $return) {
self::$lastMailboxUsedCache = $currentMailboxName;
}
return $return;
}
}

View file

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

View file

@ -0,0 +1,241 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use DateTimeInterface;
use Ddeboer\Imap\Exception\ImapNumMsgException;
use Ddeboer\Imap\Exception\ImapStatusException;
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
{
private ImapResourceInterface $resource;
private string $name;
private \stdClass $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;
}
public function getName(): string
{
return $this->name;
}
public function getEncodedName(): string
{
/** @var string $name */
$name = $this->info->name;
return (string) \preg_replace('/^{.+}/', '', $name);
}
public function getFullEncodedName(): string
{
return $this->info->name;
}
public function getAttributes(): int
{
return $this->info->attributes;
}
public function getDelimiter(): string
{
return $this->info->delimiter;
}
#[\ReturnTypeWillChange]
public function count()
{
$return = \imap_num_msg($this->resource->getStream());
if (false === $return) {
throw new ImapNumMsgException('imap_num_msg failed');
}
return $return;
}
public function getStatus(int $flags = null): \stdClass
{
$return = \imap_status($this->resource->getStream(), $this->getFullEncodedName(), $flags ?? \SA_ALL);
if (false === $return) {
throw new ImapStatusException('imap_status failed');
}
return $return;
}
public function setFlag(string $flag, $numbers): bool
{
return \imap_setflag_full($this->resource->getStream(), $this->prepareMessageIds($numbers), $flag, \ST_UID);
}
public function clearFlag(string $flag, $numbers): bool
{
return \imap_clearflag_full($this->resource->getStream(), $this->prepareMessageIds($numbers), $flag, \ST_UID);
}
public function getMessages(ConditionInterface $search = null, int $sortCriteria = null, bool $descending = false, string $charset = null): MessageIteratorInterface
{
if (null === $search) {
$search = new All();
}
$query = $search->toString();
if (\PHP_VERSION_ID < 80000) {
$descending = (int) $descending;
}
// We need to clear the stack to know whether imap_last_error()
// is related to this imap_search
\imap_errors();
if (null !== $sortCriteria) {
$params = [
$this->resource->getStream(),
$sortCriteria,
$descending,
\SE_UID,
$query,
];
if (null !== $charset) {
$params[] = $charset;
}
$messageNumbers = \imap_sort(...$params);
} else {
$params = [
$this->resource->getStream(),
$query,
\SE_UID,
];
if (null !== $charset) {
$params[] = $charset;
}
$messageNumbers = \imap_search(...$params);
}
if (false !== \imap_last_error()) {
// this way all errors occurred during search will be reported
throw new InvalidSearchCriteriaException(
\sprintf('Invalid search criteria [%s]', $query)
);
}
if (false === $messageNumbers) {
// imap_search can also return false
$messageNumbers = [];
}
return new MessageIterator($this->resource, $messageNumbers);
}
public function getMessageSequence(string $sequence): MessageIteratorInterface
{
\imap_errors();
$overview = \imap_fetch_overview($this->resource->getStream(), $sequence, \FT_UID);
if (false !== \imap_last_error()) {
throw new InvalidSearchCriteriaException(
\sprintf('Invalid sequence [%s]', $sequence)
);
}
if (\is_array($overview) && [] !== $overview) {
$messageNumbers = \array_column($overview, 'uid');
} else {
$messageNumbers = [];
}
return new MessageIterator($this->resource, $messageNumbers);
}
public function getMessage(int $number): MessageInterface
{
return new Message($this->resource, $number);
}
public function getIterator(): MessageIteratorInterface
{
return $this->getMessages();
}
public function addMessage(string $message, string $options = null, DateTimeInterface $internalDate = null): bool
{
$arguments = [
$this->resource->getStream(),
$this->getFullEncodedName(),
$message,
$options ?? '',
];
if (null !== $internalDate) {
$arguments[] = $internalDate->format('d-M-Y H:i:s O');
}
return \imap_append(...$arguments);
}
public function getThread(): array
{
\set_error_handler(static function (): bool {
return true;
});
/** @var array<string, int>|false $tree */
$tree = \imap_thread($this->resource->getStream(), \SE_UID);
\restore_error_handler();
return false !== $tree ? $tree : [];
}
public function move($numbers, MailboxInterface $mailbox): void
{
if (!\imap_mail_copy($this->resource->getStream(), $this->prepareMessageIds($numbers), $mailbox->getEncodedName(), \CP_UID | \CP_MOVE)) {
throw new MessageMoveException(\sprintf('Messages cannot be moved to "%s"', $mailbox->getName()));
}
}
public function copy($numbers, MailboxInterface $mailbox): void
{
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<int, int|string>|MessageIterator|string $messageIds Message numbers
*/
private function prepareMessageIds($messageIds): string
{
if ($messageIds instanceof MessageIterator) {
$messageIds = $messageIds->getArrayCopy();
}
if (\is_array($messageIds)) {
$messageIds = \implode(',', $messageIds);
}
return $messageIds;
}
}

View file

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

View file

@ -0,0 +1,267 @@
<?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
{
private bool $messageNumberVerified = false;
private int $imapMsgNo = 0;
private bool $structureLoaded = false;
private ?Message\Headers $headers = null;
private ?string $rawHeaders = null;
private ?string $rawMessage = null;
/**
* 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());
}
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);
}
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;
}
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;
}
public function getRawMessage(): string
{
if (null === $this->rawMessage) {
$this->rawMessage = $this->doGetContent('');
}
return $this->rawMessage;
}
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;
}
public function isRecent(): ?string
{
$recent = $this->getHeaders()->get('recent');
\assert(null === $recent || \is_string($recent));
return $recent;
}
public function isUnseen(): bool
{
return 'U' === $this->getHeaders()->get('unseen');
}
public function isFlagged(): bool
{
return 'F' === $this->getHeaders()->get('flagged');
}
public function isAnswered(): bool
{
return 'A' === $this->getHeaders()->get('answered');
}
public function isDeleted(): bool
{
return 'D' === $this->getHeaders()->get('deleted');
}
public function isDraft(): bool
{
return 'X' === $this->getHeaders()->get('draft');
}
public function isSeen(): bool
{
return 'N' !== $this->getHeaders()->get('recent') && 'U' !== $this->getHeaders()->get('unseen');
}
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();
}
public function markAsSeen(): bool
{
return $this->setFlag('\\Seen');
}
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()));
}
}
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()));
}
}
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()));
}
}
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()));
}
}
public function setFlag(string $flag): bool
{
$result = \imap_setflag_full($this->resource->getStream(), (string) $this->getNumber(), $flag, \ST_UID);
$this->clearHeaders();
return $result;
}
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,308 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
use Ddeboer\Imap\Exception\InvalidDateHeaderException;
abstract class AbstractMessage extends AbstractPart
{
/**
* @var null|Attachment[]
*/
private ?array $attachments = null;
/**
* Get message headers.
*/
abstract public function getHeaders(): Headers;
/**
* Get message id.
*
* A unique message id in the form <...>
*/
final public function getId(): ?string
{
$messageId = $this->getHeaders()->get('message_id');
\assert(null === $messageId || \is_string($messageId));
return $messageId;
}
/**
* Get message sender (from headers).
*/
final public function getFrom(): ?EmailAddress
{
$from = $this->getHeaders()->get('from');
\assert(null === $from || \is_array($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
{
$emails = $this->getHeaders()->get('to');
\assert(null === $emails || \is_array($emails));
return $this->decodeEmailAddresses($emails ?? []);
}
/**
* Get Cc recipients.
*
* @return EmailAddress[] Empty array in case message has no CC: recipients
*/
final public function getCc(): array
{
$emails = $this->getHeaders()->get('cc');
\assert(null === $emails || \is_array($emails));
return $this->decodeEmailAddresses($emails ?? []);
}
/**
* Get Bcc recipients.
*
* @return EmailAddress[] Empty array in case message has no BCC: recipients
*/
final public function getBcc(): array
{
$emails = $this->getHeaders()->get('bcc');
\assert(null === $emails || \is_array($emails));
return $this->decodeEmailAddresses($emails ?? []);
}
/**
* Get Reply-To recipients.
*
* @return EmailAddress[] Empty array in case message has no Reply-To: recipients
*/
final public function getReplyTo(): array
{
$emails = $this->getHeaders()->get('reply_to');
\assert(null === $emails || \is_array($emails));
return $this->decodeEmailAddresses($emails ?? []);
}
/**
* Get Sender.
*
* @return EmailAddress[] Empty array in case message has no Sender: recipients
*/
final public function getSender(): array
{
$emails = $this->getHeaders()->get('sender');
\assert(null === $emails || \is_array($emails));
return $this->decodeEmailAddresses($emails ?? []);
}
/**
* Get Return-Path.
*
* @return EmailAddress[] Empty array in case message has no Return-Path: recipients
*/
final public function getReturnPath(): array
{
$emails = $this->getHeaders()->get('return_path');
\assert(null === $emails || \is_array($emails));
return $this->decodeEmailAddresses($emails ?? []);
}
/**
* Get date (from headers).
*/
final public function getDate(): ?\DateTimeImmutable
{
/** @var null|string $dateHeader */
$dateHeader = $this->getHeaders()->get('date');
if (null === $dateHeader) {
return null;
}
$alteredValue = $dateHeader;
$alteredValue = \str_replace(',', '', $alteredValue);
$alteredValue = (string) \preg_replace('/^[a-zA-Z]+ ?/', '', $alteredValue);
$alteredValue = (string) \preg_replace('/\(.*\)/', '', $alteredValue);
$alteredValue = (string) \preg_replace('/\<.*\>/', '', $alteredValue);
$alteredValue = (string) \preg_replace('/\bUT\b/', 'UTC', $alteredValue);
if (0 === \preg_match('/\d\d:\d\d:\d\d.* [\+\-]\d\d:?\d\d/', $alteredValue)) {
$alteredValue .= ' +0000';
}
// Handle numeric months
$alteredValue = (string) \preg_replace('/^(\d\d) (\d\d) (\d\d(?:\d\d)?) /', '$3-$2-$1 ', $alteredValue);
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()
{
$size = $this->getHeaders()->get('size');
\assert(null === $size || \is_int($size) || \is_string($size));
return $size;
}
/**
* Get message subject (from headers).
*/
final public function getSubject(): ?string
{
$subject = $this->getHeaders()->get('subject');
\assert(null === $subject || \is_string($subject));
return $subject;
}
/**
* Get message In-Reply-To (from headers).
*
* @return string[]
*/
final public function getInReplyTo(): array
{
$inReplyTo = $this->getHeaders()->get('in_reply_to');
\assert(null === $inReplyTo || \is_string($inReplyTo));
return null !== $inReplyTo ? \explode(' ', $inReplyTo) : [];
}
/**
* Get message References (from headers).
*
* @return string[]
*/
final public function getReferences(): array
{
$references = $this->getHeaders()->get('references');
\assert(null === $references || \is_string($references));
return null !== $references ? \explode(' ', $references) : [];
}
/**
* Get body HTML.
*/
final public function getBodyHtml(): ?string
{
$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.
*/
final public function getBodyText(): ?string
{
$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) {
$this->attachments = self::gatherAttachments($this);
}
return $this->attachments;
}
/**
* @param PartInterface<PartInterface> $part
*
* @return Attachment[]
*/
private static function gatherAttachments(PartInterface $part): array
{
$attachments = [];
foreach ($part->getParts() as $childPart) {
if ($childPart instanceof Attachment) {
$attachments[] = $childPart;
}
if ($childPart->hasChildren()) {
$attachments = \array_merge($attachments, self::gatherAttachments($childPart));
}
}
return $attachments;
}
/**
* Does this message have attachments?
*/
final public function hasAttachments(): bool
{
return \count($this->getAttachments()) > 0;
}
/**
* @param \stdClass[] $addresses
*
* @return EmailAddress[]
*/
private function decodeEmailAddresses(array $addresses): array
{
$return = [];
foreach ($addresses as $address) {
if (isset($address->mailbox)) {
$return[] = $this->decodeEmailAddress($address);
}
}
return $return;
}
private function decodeEmailAddress(\stdClass $value): EmailAddress
{
return new EmailAddress($value->mailbox, $value->host, $value->personal);
}
}

View file

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

View file

@ -0,0 +1,53 @@
<?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
{
public function getFilename(): ?string
{
$filename = $this->getParameters()->get('filename');
if (null === $filename || '' === $filename) {
$filename = $this->getParameters()->get('name');
}
\assert(null === $filename || \is_string($filename));
return $filename;
}
public function getSize()
{
$size = $this->getParameters()->get('size');
if (\is_numeric($size)) {
$size = (int) $size;
}
\assert(null === $size || \is_int($size));
return $size;
}
public function isEmbeddedMessage(): bool
{
return self::TYPE_MESSAGE === $this->getType();
}
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;
use Ddeboer\Imap\Exception\NotEmbeddedMessageException;
/**
* An e-mail attachment.
*/
interface AttachmentInterface extends PartInterface
{
/**
* Get attachment filename.
*/
public function getFilename(): ?string;
/**
* Get attachment file size.
*
* @return null|int Number of bytes
*/
public function getSize();
/**
* Is this attachment also an Embedded Message?
*/
public function isEmbeddedMessage(): bool;
/**
* Return embedded message.
*
* @throws NotEmbeddedMessageException
*
* @return EmbeddedMessageInterface<PartInterface>
*/
public function getEmbeddedMessage(): EmbeddedMessageInterface;
}

View file

@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
interface BasicMessageInterface extends PartInterface
{
/**
* Get raw message headers.
*/
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.
*/
public function getHeaders(): Headers;
/**
* Get message id.
*
* A unique message id in the form <...>
*/
public function getId(): ?string;
/**
* Get message sender (from headers).
*/
public function getFrom(): ?EmailAddress;
/**
* 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).
*/
public function getDate(): ?\DateTimeImmutable;
/**
* Get message size (from headers).
*
* @return null|int|string
*/
public function getSize();
/**
* Get message subject (from headers).
*/
public function getSubject(): ?string;
/**
* Get message In-Reply-To (from headers).
*
* @return string[]
*/
public function getInReplyTo(): array;
/**
* Get message References (from headers).
*
* @return string[]
*/
public function getReferences(): array;
/**
* Get body HTML.
*
* @return null|string Null if message has no HTML message part
*/
public function getBodyHtml(): ?string;
/**
* Get body text.
*/
public function getBodyText(): ?string;
/**
* Get attachments (if any) linked to this e-mail.
*
* @return AttachmentInterface[]
*/
public function getAttachments(): array;
/**
* Does this message have attachments?
*/
public function hasAttachments(): bool;
}

View file

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
/**
* An e-mail address.
*/
final class EmailAddress
{
private string $mailbox;
private ?string $hostname;
private ?string $name;
private ?string $address;
public function __construct(string $mailbox, string $hostname = null, string $name = null)
{
$this->mailbox = $mailbox;
$this->hostname = $hostname;
$this->name = $name;
$this->address = null;
if (null !== $hostname) {
$this->address = $mailbox . '@' . $hostname;
}
}
/**
* @return null|string
*/
public function getAddress()
{
return $this->address;
}
/**
* Returns address with person name.
*/
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;
}
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,53 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
final class EmbeddedMessage extends AbstractMessage implements EmbeddedMessageInterface
{
private ?Headers $headers = null;
private ?string $rawHeaders = null;
private ?string $rawMessage = null;
public function getHeaders(): Headers
{
if (null === $this->headers) {
$this->headers = new Headers(\imap_rfc822_parse_headers($this->getRawHeaders()));
}
return $this->headers;
}
public function getRawHeaders(): string
{
if (null === $this->rawHeaders) {
$rawHeaders = \explode("\r\n\r\n", $this->getRawMessage(), 2);
$this->rawHeaders = \current($rawHeaders);
}
return $this->rawHeaders;
}
public function getRawMessage(): string
{
if (null === $this->rawMessage) {
$this->rawMessage = $this->doGetContent($this->getPartNumber());
}
return $this->rawMessage;
}
/**
* Get content part number.
*/
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,71 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
final class Headers extends Parameters
{
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.
*
* @return null|int|\stdClass[]|string
*/
public function get(string $key)
{
return parent::get(\strtolower($key));
}
/**
* Parse header.
*
* @param int|\stdClass[]|string $value
*
* @return int|\stdClass[]|string
*/
private function parseHeader(string $key, $value)
{
switch ($key) {
case 'msgno':
\assert(\is_string($value));
return (int) $value;
case 'from':
case 'to':
case 'cc':
case 'bcc':
case 'reply_to':
case 'sender':
case 'return_path':
\assert(\is_array($value));
/** @var \stdClass $address */
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':
\assert(\is_string($value));
return $this->decode($value);
}
return $value;
}
}

View file

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
/**
* @extends \ArrayIterator<int|string, int|string|\stdClass[]>
*/
class Parameters extends \ArrayIterator
{
/**
* @var array<string, string>
*/
private static array $attachmentCustomKeys = [
'name*' => 'name',
'filename*' => 'filename',
];
/**
* @param \stdClass[] $parameters
*/
public function add(array $parameters = []): void
{
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;
}
}
/**
* @return null|int|\stdClass[]|string
*/
public function get(string $key)
{
return $this[$key] ?? null;
}
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 = 1 === \preg_match('#[^%a-zA-Z0-9\-_\.\+]#', $matches['urltext']);
$hasEscapedChars = 1 === \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,112 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
/**
* A message part.
*/
interface PartInterface extends \RecursiveIterator
{
public const TYPE_TEXT = 'text';
public const TYPE_MULTIPART = 'multipart';
public const TYPE_MESSAGE = 'message';
public const TYPE_APPLICATION = 'application';
public const TYPE_AUDIO = 'audio';
public const TYPE_IMAGE = 'image';
public const TYPE_VIDEO = 'video';
public const TYPE_MODEL = 'model';
public const TYPE_OTHER = 'other';
public const TYPE_UNKNOWN = 'unknown';
public const ENCODING_7BIT = '7bit';
public const ENCODING_8BIT = '8bit';
public const ENCODING_BINARY = 'binary';
public const ENCODING_BASE64 = 'base64';
public const ENCODING_QUOTED_PRINTABLE = 'quoted-printable';
public const ENCODING_UNKNOWN = 'unknown';
public const SUBTYPE_PLAIN = 'PLAIN';
public const SUBTYPE_HTML = 'HTML';
public const SUBTYPE_RFC822 = 'RFC822';
/**
* Get message number (from headers).
*/
public function getNumber(): int;
/**
* Part charset.
*/
public function getCharset(): ?string;
/**
* Part type.
*/
public function getType(): ?string;
/**
* Part subtype.
*/
public function getSubtype(): ?string;
/**
* Part encoding.
*/
public function getEncoding(): ?string;
/**
* Part disposition.
*/
public function getDisposition(): ?string;
/**
* Part description.
*/
public function getDescription(): ?string;
/**
* Part bytes.
*
* @return null|int|string
*/
public function getBytes();
/**
* Part lines.
*/
public function getLines(): ?string;
/**
* Part parameters.
*/
public function getParameters(): Parameters;
/**
* Get raw part content.
*/
public function getContent(): string;
/**
* Get decoded part content.
*/
public function getDecodedContent(): string;
/**
* Part structure.
*/
public function getStructure(): \stdClass;
/**
* Get part number.
*/
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,333 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
use Ddeboer\Imap\Exception\UnsupportedCharsetException;
final class Transcoder
{
/**
* @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 const CHARSET_ALIASES = [
'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',
'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-gbk' => 'gbk',
'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-mac-ce' => 'windows-1250',
'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
*/
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::CHARSET_ALIASES[$lowercaseFromCharset])) {
$fromCharset = self::CHARSET_ALIASES[$lowercaseFromCharset];
}
\set_error_handler(static function (): bool {
return true;
});
$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(static function ($nr, $message) use (&$errorMessage, &$errorNumber): bool {
$errorMessage = $message;
$errorNumber = $nr;
return true;
});
$decodedText = '';
try {
$decodedText = \mb_convert_encoding($text, 'UTF-8', $fromCharset);
} catch (\Error $error) {
$errorMessage = $error->getMessage();
}
\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,110 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use Ddeboer\Imap\Exception\MessageCopyException;
use Ddeboer\Imap\Exception\MessageDeleteException;
use Ddeboer\Imap\Exception\MessageMoveException;
use Ddeboer\Imap\Exception\MessageUndeleteException;
/**
* An IMAP message (e-mail).
*/
interface MessageInterface extends Message\BasicMessageInterface
{
/**
* Get raw part content.
*/
public function getContent(): string;
/**
* Get message recent flag value (from headers).
*/
public function isRecent(): ?string;
/**
* Get message unseen flag value (from headers).
*/
public function isUnseen(): bool;
/**
* Get message flagged flag value (from headers).
*/
public function isFlagged(): bool;
/**
* Get message answered flag value (from headers).
*/
public function isAnswered(): bool;
/**
* Get message deleted flag value (from headers).
*/
public function isDeleted(): bool;
/**
* Get message draft flag value (from headers).
*/
public function isDraft(): bool;
/**
* Has the message been marked as read?
*/
public function isSeen(): bool;
/**
* Mark message as seen.
*
* @deprecated since version 1.1, to be removed in 2.0
*/
public function maskAsSeen(): bool;
/**
* Mark message as seen.
*/
public function markAsSeen(): bool;
/**
* Move message to another mailbox.
*
* @throws MessageCopyException
*/
public function copy(MailboxInterface $mailbox): void;
/**
* Move message to another mailbox.
*
* @throws MessageMoveException
*/
public function move(MailboxInterface $mailbox): void;
/**
* Delete message.
*
* @throws MessageDeleteException
*/
public function delete(): void;
/**
* Undelete message.
*
* @throws MessageUndeleteException
*/
public function undelete(): void;
/**
* Set Flag Message.
*
* @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
*/
public function setFlag(string $flag): bool;
/**
* Clear Flag Message.
*
* @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
*/
public function clearFlag(string $flag): bool;
}

View file

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use Ddeboer\Imap\Message\PartInterface;
/**
* @extends \ArrayIterator<int, MessageInterface>
*/
final class MessageIterator extends \ArrayIterator implements MessageIteratorInterface
{
private ImapResourceInterface $resource;
/**
* Constructor.
*
* @param ImapResourceInterface $resource IMAP resource
* @param int[] $messageNumbers Array of message numbers
*/
public function __construct(ImapResourceInterface $resource, array $messageNumbers)
{
$this->resource = $resource;
parent::__construct($messageNumbers);
}
/**
* Get current message.
*
* @return MessageInterface<PartInterface>
*/
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,20 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use Ddeboer\Imap\Message\PartInterface;
/**
* @extends \Iterator<MessageInterface>
*/
interface MessageIteratorInterface extends \Iterator, \Countable
{
/**
* Get current message.
*
* @return MessageInterface<PartInterface>
*/
public function current(): MessageInterface;
}

View file

@ -0,0 +1,47 @@
<?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.
*/
private string $dateFormat;
/**
* The date to be used for the condition.
*/
private DateTimeInterface $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.
*/
final public function toString(): string
{
return \sprintf('%s "%s"', $this->getKeyword(), $this->date->format($this->dateFormat));
}
/**
* Returns the keyword that the condition represents.
*/
abstract protected function getKeyword(): string;
}

View file

@ -0,0 +1,40 @@
<?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.
*/
private string $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.
*/
final public function toString(): string
{
return \sprintf('%s "%s"', $this->getKeyword(), $this->text);
}
/**
* Returns the keyword that the condition represents.
*/
abstract protected function getKeyword(): string;
}

View file

@ -0,0 +1,16 @@
<?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.
*/
public function toString(): string;
}

View file

@ -0,0 +1,22 @@
<?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.
*/
protected function getKeyword(): string
{
return 'BEFORE';
}
}

View file

@ -0,0 +1,22 @@
<?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.
*/
protected function getKeyword(): string
{
return 'ON';
}
}

View file

@ -0,0 +1,22 @@
<?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.
*/
protected function getKeyword(): string
{
return 'SINCE';
}
}

View file

@ -0,0 +1,23 @@
<?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.
*/
protected function getKeyword(): string
{
return 'BCC';
}
}

View file

@ -0,0 +1,23 @@
<?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.
*/
protected function getKeyword(): string
{
return 'CC';
}
}

View file

@ -0,0 +1,22 @@
<?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.
*/
protected function getKeyword(): string
{
return 'FROM';
}
}

View file

@ -0,0 +1,23 @@
<?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.
*/
protected function getKeyword(): string
{
return 'TO';
}
}

View file

@ -0,0 +1,22 @@
<?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.
*/
public function toString(): string
{
return 'ANSWERED';
}
}

View file

@ -0,0 +1,22 @@
<?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.
*/
public function toString(): string
{
return 'FLAGGED';
}
}

View file

@ -0,0 +1,22 @@
<?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.
*/
public function toString(): string
{
return 'RECENT';
}
}

View file

@ -0,0 +1,22 @@
<?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.
*/
public function toString(): string
{
return 'SEEN';
}
}

View file

@ -0,0 +1,22 @@
<?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.
*/
public function toString(): string
{
return 'UNANSWERED';
}
}

View file

@ -0,0 +1,22 @@
<?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.
*/
public function toString(): string
{
return 'UNFLAGGED';
}
}

View file

@ -0,0 +1,22 @@
<?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.
*/
public function toString(): string
{
return 'UNSEEN';
}
}

View file

@ -0,0 +1,22 @@
<?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.
*/
public function toString(): string
{
return 'ALL';
}
}

View file

@ -0,0 +1,55 @@
<?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 ConditionInterface[]
*/
private array $conditions = [];
/**
* @param ConditionInterface[] $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
*
* @return void
*/
private function addCondition(ConditionInterface $condition)
{
$this->conditions[] = $condition;
}
/**
* Returns the keyword that the condition represents.
*/
public function toString(): string
{
$conditions = \array_map(static function (ConditionInterface $condition): string {
return $condition->toString();
}, $this->conditions);
return \sprintf('( %s )', \implode(' OR ', $conditions));
}
}

View file

@ -0,0 +1,29 @@
<?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.
*/
private string $expression;
/**
* @param string $expression text for the condition
*/
public function __construct(string $expression)
{
$this->expression = $expression;
}
public function toString(): string
{
return $this->expression;
}
}

View file

@ -0,0 +1,22 @@
<?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.
*/
public function toString(): string
{
return 'DELETED';
}
}

View file

@ -0,0 +1,21 @@
<?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.
*/
public function toString(): string
{
return 'NEW';
}
}

View file

@ -0,0 +1,21 @@
<?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.
*/
public function toString(): string
{
return 'OLD';
}
}

View file

@ -0,0 +1,22 @@
<?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.
*/
public function toString(): string
{
return 'UNDELETED';
}
}

View file

@ -0,0 +1,22 @@
<?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.
*/
protected function getKeyword(): string
{
return 'BODY';
}
}

View file

@ -0,0 +1,22 @@
<?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.
*/
protected function getKeyword(): string
{
return 'KEYWORD';
}
}

View file

@ -0,0 +1,22 @@
<?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.
*/
protected function getKeyword(): string
{
return 'SUBJECT';
}
}

View file

@ -0,0 +1,22 @@
<?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.
*/
protected function getKeyword(): string
{
return 'TEXT';
}
}

View file

@ -0,0 +1,22 @@
<?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.
*/
protected function getKeyword(): string
{
return 'UNKEYWORD';
}
}

View file

@ -0,0 +1,44 @@
<?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 ConditionInterface[]
*/
private array $conditions = [];
/**
* Adds a new condition to the expression.
*
* @param ConditionInterface $condition the condition to be added
*/
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.
*/
public function toString(): string
{
$conditions = \array_map(static function (ConditionInterface $condition): string {
return $condition->toString();
}, $this->conditions);
return \implode(' ', $conditions);
}
}

View file

@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use Ddeboer\Imap\Exception\AuthenticationFailedException;
use Ddeboer\Imap\Exception\ResourceCheckFailureException;
/**
* An IMAP server.
*/
final class Server implements ServerInterface
{
private string $hostname;
private string $port;
private string $flags;
/**
* @var mixed[]
*/
private array $parameters;
private int $options;
private int $retries;
/**
* 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 mixed[] $parameters Connection parameters
* @param int $options Connection options
* @param int $retries Retries number
*/
public function __construct(
string $hostname,
string $port = '993',
string $flags = '/imap/ssl/validate-cert',
array $parameters = [],
int $options = 0,
int $retries = 1
) {
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;
$this->options = $options;
$this->retries = $retries;
}
/**
* Authenticate connection.
*
* @param string $username Username
* @param string $password Password
*
* @throws AuthenticationFailedException
*/
public function authenticate(string $username, string $password): ConnectionInterface
{
$errorMessage = null;
$errorNumber = 0;
\set_error_handler(static function ($nr, $message) use (&$errorMessage, &$errorNumber): bool {
$errorMessage = $message;
$errorNumber = $nr;
return true;
});
$resource = \imap_open(
$this->getServerString(),
$username,
$password,
$this->options,
$this->retries,
$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);
if (false === $check) {
throw new ResourceCheckFailureException('Resource check failure');
}
$mailbox = $check->Mailbox;
$connection = $mailbox;
$curlyPosition = \strpos($mailbox, '}');
if (false !== $curlyPosition) {
$connection = \substr($mailbox, 0, $curlyPosition + 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.
*/
private function getServerString(): string
{
return \sprintf(
'{%s%s%s}',
$this->hostname,
'' !== $this->port ? ':' . $this->port : '',
$this->flags
);
}
}

View file

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

View file

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