mirror of
https://github.com/OpenXE-org/OpenXE.git
synced 2024-12-27 15:10:28 +01:00
447 lines
12 KiB
PHP
447 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Xentral\Components\MailClient\Client;
|
|
|
|
use Exception;
|
|
use Laminas\Mail\Header\Cc;
|
|
use Laminas\Mail\Header\From;
|
|
use Laminas\Mail\Header\To;
|
|
use Laminas\Mail\Headers;
|
|
use Laminas\Mail\Protocol\Imap as Protocol;
|
|
use Laminas\Mail\Storage\Imap as ImapClient;
|
|
use Laminas\Mail\Storage\Message;
|
|
use Laminas\Mail\Storage\Part;
|
|
use Xentral\Components\MailClient\Config\ImapMailClientConfig;
|
|
use Xentral\Components\MailClient\Config\ImapMailClientConfigInterface;
|
|
use Xentral\Components\MailClient\Data\MailBoxInfoData;
|
|
use Xentral\Components\MailClient\Data\MailMessageData;
|
|
use Xentral\Components\MailClient\Data\MailMessageHeaderValue;
|
|
use Xentral\Components\MailClient\Data\MailMessageInterface;
|
|
use Xentral\Components\MailClient\Data\MailMessagePartData;
|
|
use Xentral\Components\MailClient\Data\MailMessagePartInterface;
|
|
use Xentral\Components\MailClient\Exception\ClientConnectionException;
|
|
use Xentral\Components\MailClient\Exception\FolderNotFoundException;
|
|
use Xentral\Components\MailClient\Exception\InvalidArgumentException;
|
|
use Xentral\Components\MailClient\Exception\LoginException;
|
|
use Xentral\Components\MailClient\Exception\MessageNotFoundException;
|
|
use Xentral\Components\MailClient\Exception\OAuthException;
|
|
use Xentral\Components\MailClient\Exception\ProtocolException;
|
|
use Xentral\Components\Mailer\Data\EmailRecipient;
|
|
|
|
final class ImapMailClient implements MailClientInterface
|
|
{
|
|
/** @var ImapMailClientConfigInterface $config */
|
|
private $config;
|
|
|
|
/** @var Protocol $protocol */
|
|
private $protocol;
|
|
|
|
/** @var ImapClient $imap */
|
|
private $imap;
|
|
|
|
/**
|
|
* @param ImapMailClientConfigInterface $config
|
|
*/
|
|
public function __construct(ImapMailClientConfigInterface $config)
|
|
{
|
|
$this->config = $config;
|
|
}
|
|
|
|
/**
|
|
* @throws LoginException
|
|
*
|
|
* @return void
|
|
*/
|
|
public function connect(): void
|
|
{
|
|
$ssl = '';
|
|
if ($this->config->isSslEnabled()) {
|
|
$ssl = 'ssl';
|
|
}
|
|
$this->protocol = new Protocol(
|
|
$this->config->getServer(),
|
|
$this->config->getPort(),
|
|
$ssl
|
|
);
|
|
switch (strtolower($this->config->getAuthType())) {
|
|
case ImapMailClientConfig::AUTH_BASIC:
|
|
$this->protocol->login($this->config->getUser(), $this->config->getPassword());
|
|
break;
|
|
|
|
case ImapMailClientConfig::AUTH_XOAUTH2:
|
|
$this->loginOauth();
|
|
break;
|
|
|
|
default:
|
|
throw new LoginException(
|
|
sprintf('Authentication method "%s" not supported', $this->config->getAuthType())
|
|
);
|
|
}
|
|
$this->imap = new ImapClient($this->protocol);
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public function disconnect(): void
|
|
{
|
|
if ($this->protocol === null) {
|
|
return;
|
|
}
|
|
$this->protocol->logout();
|
|
}
|
|
|
|
/**
|
|
* @param string $criteria
|
|
*
|
|
* @throws InvalidArgumentException
|
|
* @throws ClientConnectionException
|
|
*
|
|
* @return array
|
|
*/
|
|
public function searchMessages(string $criteria): array
|
|
{
|
|
$this->ensureConnection();
|
|
$criteriaArray = preg_split('/\s/', $criteria);
|
|
$result = $this->protocol->search($criteriaArray);
|
|
if ($result === null) {
|
|
throw new InvalidArgumentException(sprintf('Invalid search criteria "%s".', $criteria));
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @param int $msgNumber
|
|
*
|
|
* @throws MessageNotFoundException
|
|
* @throws ClientConnectionException
|
|
*
|
|
* @return MailMessageInterface
|
|
*/
|
|
public function fetchMessage(int $msgNumber): MailMessageInterface
|
|
{
|
|
$this->ensureConnection();
|
|
try {
|
|
$message = $this->imap->getMessage($msgNumber);
|
|
} catch (Exception $e) {
|
|
throw new MessageNotFoundException(
|
|
sprintf('Message number %s not found.', $msgNumber)
|
|
);
|
|
}
|
|
|
|
return $this->parseMessage($message);
|
|
}
|
|
|
|
/**
|
|
* @param int $msgNumber
|
|
* @param string $targetFolder
|
|
*
|
|
* @throws ClientConnectionException
|
|
*
|
|
* @return void
|
|
*/
|
|
public function copyMessage(int $msgNumber, string $targetFolder): void
|
|
{
|
|
$this->ensureConnection();
|
|
try {
|
|
$this->imap->copyMessage($msgNumber, $targetFolder);
|
|
} catch (Exception $e) {
|
|
throw new ProtocolException(
|
|
sprintf('Failed to copy Message "%s" to Folder "%s"', $msgNumber, $targetFolder),
|
|
$e->getCode(),
|
|
$e
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param int $msgNumber
|
|
*
|
|
* @throws ProtocolException
|
|
* @throws ClientConnectionException
|
|
*
|
|
* @return void
|
|
*/
|
|
public function deleteMessage(int $msgNumber): void
|
|
{
|
|
$this->ensureConnection();
|
|
try {
|
|
$this->imap->removeMessage($msgNumber);
|
|
} catch (Exception $e) {
|
|
throw new ProtocolException('Failed do delete Message.', $e->getCode(), $e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $inbox
|
|
*
|
|
* @throws FolderNotFoundException
|
|
* @throws ClientConnectionException
|
|
*
|
|
* @return MailBoxInfoData
|
|
*/
|
|
public function examineInbox(string $inbox = null): MailBoxInfoData
|
|
{
|
|
$this->ensureConnection();
|
|
if ($inbox === null) {
|
|
$inbox = $this->config->getInboxFolder();
|
|
}
|
|
$status = $this->protocol->examine($inbox);
|
|
if ($status === false) {
|
|
throw new FolderNotFoundException(
|
|
sprintf('Cannot examine "%s" - folder probably not existing.', $inbox)
|
|
);
|
|
}
|
|
|
|
return new MailBoxInfoData(
|
|
(int)$status['exists'],
|
|
(int)$status['recent'],
|
|
(int)$status['uidvalidity'],
|
|
$status['flags'][0]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @throws ClientConnectionException
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function expunge(): bool
|
|
{
|
|
$this->ensureConnection();
|
|
$result = $this->protocol->expunge();
|
|
|
|
return $result === true;
|
|
}
|
|
|
|
/**
|
|
* Sets Flags on message.
|
|
*
|
|
* @param int $msgNumber
|
|
* @param string[] $flags values: '\Seen' '\Answered' '\Flagged' '\Deleted' '\Draft'
|
|
*
|
|
* @throws ProtocolException
|
|
* @throws ClientConnectionException
|
|
*
|
|
* @return void
|
|
*/
|
|
public function setFlags(int $msgNumber, array $flags): void
|
|
{
|
|
$this->ensureConnection();
|
|
try {
|
|
$this->imap->setFlags($msgNumber, $flags);
|
|
} catch (Exception $e) {
|
|
throw new ProtocolException($e->getMessage(), $e->getCode(), $e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $folder
|
|
*
|
|
* @throws FolderNotFoundException
|
|
* @throws ClientConnectionException
|
|
*
|
|
* @return void
|
|
*/
|
|
public function selectFolder(string $folder): void
|
|
{
|
|
$this->ensureConnection();
|
|
try {
|
|
$this->imap->selectFolder($folder);
|
|
} catch (Exception $e) {
|
|
throw new FolderNotFoundException($e->getMessage(), $e->getCode(), $e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @throws ProtocolException
|
|
* @throws ClientConnectionException
|
|
*
|
|
* @return void
|
|
*/
|
|
public function noop(): void
|
|
{
|
|
$this->ensureConnection();
|
|
try {
|
|
$this->imap->noop();
|
|
} catch (Exception $e) {
|
|
throw new ProtocolException('NOOP Command Failed');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $message
|
|
* @param string $targetFolder
|
|
*
|
|
* @throws ProtocolException
|
|
*
|
|
* @return void
|
|
*/
|
|
public function appendMessage(string $message, string $targetFolder): void
|
|
{
|
|
try {
|
|
$this->imap->appendMessage($message, $targetFolder);
|
|
} catch (Exception $e) {
|
|
throw new ProtocolException('Failed to append message.', $e->getCode(), $e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @throws OAuthException
|
|
*
|
|
* @return void
|
|
*/
|
|
private function loginOauth(): void
|
|
{
|
|
$authString = sprintf(
|
|
"user=%s\1auth=Bearer %s\1\1",
|
|
$this->config->getUser(),
|
|
$this->config->getPassword()
|
|
);
|
|
$authString = base64_encode($authString);
|
|
$this->protocol->sendRequest('AUTHENTICATE', ['XOAUTH2', $authString]);
|
|
|
|
while (true) {
|
|
$response = '';
|
|
$isPlus = $this->protocol->readLine($response, '+', true);
|
|
if ($isPlus) {
|
|
$this->protocol->sendRequest('');
|
|
continue;
|
|
}
|
|
if (preg_match("/^OK /i", $response)) {
|
|
return;
|
|
}
|
|
if (preg_match('/^NO (.+)/i', $response, $matches)) {
|
|
throw new LoginException(
|
|
sprintf('OAuth access denied: %s', $matches[1])
|
|
);
|
|
}
|
|
if (preg_match('/^BAD (.+)/i', $response, $matches)) {
|
|
throw new LoginException(
|
|
sprintf('OAuth login error: %s', $matches[1])
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param Message $message
|
|
*
|
|
* @return MailMessageData
|
|
*/
|
|
private function parseMessage(Message $message): MailMessageData
|
|
{
|
|
/** @var From $from */
|
|
$from = $message->getHeader('From');
|
|
$list = $from->getAddressList();
|
|
$sender = new EmailRecipient($list->current()->getEmail(), $list->current()->getName());
|
|
try {
|
|
$recipients = [];
|
|
/** @var To $toHeader */
|
|
$toHeader = $message->getHeader('To');
|
|
foreach ($toHeader->getAddressList() as $recipient) {
|
|
$recipients[] = new EmailRecipient($recipient->getEmail(), $recipient->getName());
|
|
}
|
|
} catch (Exception $e) {
|
|
$recipients = [];
|
|
}
|
|
try {
|
|
/** @var Cc $ccHeader */
|
|
$ccHeader = $message->getHeader('Cc');
|
|
$ccs = [];
|
|
foreach ($ccHeader->getAddressList() as $cc) {
|
|
$ccs[] = new EmailRecipient($cc->getEmail(), $cc->getName());
|
|
}
|
|
} catch (Exception $e) {
|
|
$ccs = [];
|
|
}
|
|
$raw = $message->getContent();
|
|
$content = null;
|
|
$parts = $this->parseMessagePartsRecursive($message);
|
|
if (count($parts) === 0) {
|
|
$content = $raw;
|
|
}
|
|
|
|
return new MailMessageData(
|
|
$sender,
|
|
$recipients,
|
|
$ccs,
|
|
$message->getFlags(),
|
|
$this->parseHeaders($message->getHeaders()),
|
|
$content,
|
|
$this->parseMessagePartsRecursive($message),
|
|
$raw
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param Headers $headers
|
|
*
|
|
* @return array
|
|
*/
|
|
private function parseHeaders(?Headers $headers): array
|
|
{
|
|
if ($headers === null) {
|
|
return [];
|
|
}
|
|
$headerArray = [];
|
|
foreach ($headers as $header) {
|
|
$key = strtolower($header->getFieldName());
|
|
$val = new MailMessageHeaderValue(
|
|
$header->getFieldName(),
|
|
$header->getFieldValue(),
|
|
$header->getEncoding()
|
|
);
|
|
$headerArray[$key] = $val;
|
|
}
|
|
|
|
return $headerArray;
|
|
}
|
|
|
|
/**
|
|
* @param Part $message
|
|
*
|
|
* @return MailMessagePartInterface[]
|
|
*/
|
|
private function parseMessagePartsRecursive(Part $message): array
|
|
{
|
|
if ($message->countParts() === 0) {
|
|
return [];
|
|
}
|
|
$parts = [];
|
|
$partsCount = (int)$message->countParts();
|
|
for ($i = 1; $i <= $partsCount; $i++) {
|
|
$part = $message->getPart($i);
|
|
$headers = $this->parseHeaders($part->getHeaders());
|
|
$content = null;
|
|
$subParts = $this->parseMessagePartsRecursive($part);
|
|
if (count($subParts) === 0) {
|
|
$content = $part->getContent();
|
|
}
|
|
|
|
$parts[] = new MailMessagePartData(
|
|
$headers,
|
|
$content,
|
|
$subParts
|
|
);
|
|
}
|
|
|
|
return $parts;
|
|
}
|
|
|
|
/**
|
|
* @throws ClientConnectionException
|
|
*
|
|
* @return void
|
|
*/
|
|
private function ensureConnection(): void
|
|
{
|
|
if ($this->protocol === null || $this->imap === null) {
|
|
throw new ClientConnectionException('IMAP client not connected.');
|
|
}
|
|
}
|
|
}
|