Initial xentral_oss_20.3.c9ffacf
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient;
|
||||
|
||||
use Xentral\Components\MailClient\Client\MimeMessageFormatter;
|
||||
use Xentral\Components\MailClient\Client\MimeMessageFormatterInterface;
|
||||
use Xentral\Core\DependencyInjection\ServiceContainer;
|
||||
|
||||
final class Bootstrap
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerServices(): array
|
||||
{
|
||||
return [
|
||||
'MailClientFactory' => 'onInitMailClientFactory',
|
||||
'MailClientMimeMessageFormatter' => 'onInitMimeMessageFormatter',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServiceContainer $container
|
||||
*
|
||||
* @return MailClientFactory
|
||||
*/
|
||||
public static function onInitMailClientFactory(ServiceContainer $container): MailClientFactory
|
||||
{
|
||||
return new MailClientFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServiceContainer $container
|
||||
*
|
||||
* @return MimeMessageFormatterInterface
|
||||
*/
|
||||
public static function onInitMimeMessageFormatter(ServiceContainer $container): MimeMessageFormatterInterface
|
||||
{
|
||||
return new MimeMessageFormatter();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
<?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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Client;
|
||||
|
||||
use Xentral\Components\MailClient\Data\MailBoxInfoData;
|
||||
use Xentral\Components\MailClient\Data\MailMessageInterface;
|
||||
|
||||
interface MailClientInterface
|
||||
{
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function connect(): void;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function disconnect(): void;
|
||||
|
||||
/**
|
||||
* @param string $criteria
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function searchMessages(string $criteria): array;
|
||||
|
||||
/**
|
||||
* @param int $msgNumber
|
||||
*
|
||||
* @return MailMessageInterface
|
||||
*/
|
||||
public function fetchMessage(int $msgNumber): MailMessageInterface;
|
||||
|
||||
/**
|
||||
* @param int $msgNumber
|
||||
* @param string $targetMailbox
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function copyMessage(int $msgNumber, string $targetMailbox): void;
|
||||
|
||||
/**
|
||||
* @param int $msgNumber
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function deleteMessage(int $msgNumber): void;
|
||||
|
||||
/**
|
||||
* @param string $folder
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function selectFolder(string $folder): void;
|
||||
|
||||
/**
|
||||
* @param string $inbox
|
||||
*
|
||||
* @return MailBoxInfoData
|
||||
*/
|
||||
public function examineInbox(string $inbox): MailBoxInfoData;
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function expunge(): bool;
|
||||
|
||||
/**
|
||||
* @param int $msgNumber
|
||||
* @param string[] $flags
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setFlags(int $msgNumber, array $flags): void;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function noop(): void;
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Client;
|
||||
|
||||
use Laminas\Mail\Header\ContentType;
|
||||
use Laminas\Mail\Header\MessageId;
|
||||
use Laminas\Mail\Message;
|
||||
use Laminas\Mime\Message as MimeMessage;
|
||||
use Laminas\Mime\Mime;
|
||||
use Laminas\Mime\Part;
|
||||
use Xentral\Components\MailClient\Exception\InvalidArgumentException;
|
||||
use Xentral\Components\MailClient\Exception\MessageFormatException;
|
||||
use Xentral\Components\Mailer\Data\EmailMessage;
|
||||
use Xentral\Components\Mailer\Data\EmailRecipient;
|
||||
use Xentral\Components\Mailer\Data\FileAttachment;
|
||||
use Xentral\Components\Mailer\Data\ImageAttachment;
|
||||
use Xentral\Components\Mailer\Data\StringAttachment;
|
||||
|
||||
final class MimeMessageFormatter implements MimeMessageFormatterInterface
|
||||
{
|
||||
/**
|
||||
* @param EmailMessage $email
|
||||
* @param EmailRecipient $from
|
||||
* @param string|null $messageId
|
||||
*
|
||||
* @throws MessageFormatException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function formatMessage(EmailMessage $email, EmailRecipient $from, string $messageId = null): string
|
||||
{
|
||||
if ($messageId !== null && !preg_match('/^<.*@.*>$/', $messageId)) {
|
||||
throw new InvalidArgumentException('message id must be RFC 5322 conform');
|
||||
}
|
||||
$message = new Message();
|
||||
$message->setEncoding('UTF-8');
|
||||
$message->addFrom($from->getEmail(), $from->getName());
|
||||
foreach ($email->getRecipients() as $recipient) {
|
||||
$message->addTo($recipient->getEmail(), $recipient->getName());
|
||||
}
|
||||
foreach ($email->getCcRecipients() as $recipient) {
|
||||
$message->addCc($recipient->getEmail(), $recipient->getName());
|
||||
}
|
||||
foreach ($email->getBccRecipients() as $recipient) {
|
||||
$message->addBcc($recipient->getEmail(), $recipient->getName());
|
||||
}
|
||||
$message->setSubject($email->getSubject());
|
||||
$message->addReplyTo($from->getEmail(), $from->getName());
|
||||
$idHeader = new MessageId();
|
||||
$idHeader->setId($messageId);
|
||||
$message->getHeaders()->addHeader($idHeader);
|
||||
$body = $this->createMessageBody($email);
|
||||
$message->setBody($body);
|
||||
|
||||
$contentType = null;
|
||||
if (count($email->getAttachments()) > 0) {
|
||||
$contentType = Mime::MULTIPART_RELATED;
|
||||
}
|
||||
if (count($email->getAttachments()) === 0 && $email->isHtml()) {
|
||||
$contentType = Mime::MULTIPART_ALTERNATIVE;
|
||||
}
|
||||
if ($contentType !== null) {
|
||||
/** @var ContentType $contentTypeHeader */
|
||||
$contentTypeHeader = $message->getHeaders()->get('Content-Type');
|
||||
$contentTypeHeader->setType($contentType);
|
||||
}
|
||||
|
||||
return $message->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EmailMessage $email
|
||||
*
|
||||
* @return MimeMessage
|
||||
*/
|
||||
private function createMessageBody(EmailMessage $email): MimeMessage
|
||||
{
|
||||
$textParts = [];
|
||||
if (!$email->isHtml()) {
|
||||
$plainText = $email->getBody();
|
||||
} else {
|
||||
$plainText = $this->convertHtmlToPlainText($email->getBody());
|
||||
}
|
||||
$textPart = new Part(Mime::encode($plainText, Mime::ENCODING_8BIT));
|
||||
$textPart->type = Mime::TYPE_TEXT;
|
||||
$textPart->charset = 'ISO-8859-1';
|
||||
$textPart->encoding = Mime::ENCODING_QUOTEDPRINTABLE;
|
||||
if ($email->isHtml()) {
|
||||
$htmlPart = new Part($email->getBody());
|
||||
$htmlPart->type = Mime::TYPE_HTML;
|
||||
$htmlPart->charset = 'ISO-8859-1';
|
||||
$htmlPart->encoding = Mime::ENCODING_QUOTEDPRINTABLE;
|
||||
$textParts[] = $htmlPart;
|
||||
}
|
||||
$textParts[] = $textPart;
|
||||
$attachmentParts = [];
|
||||
foreach ($email->getAttachments() as $attachment) {
|
||||
$contentStream = null;
|
||||
$disposition = null;
|
||||
switch (get_class($attachment)) {
|
||||
case FileAttachment::class:
|
||||
$content = file_get_contents($attachment->getPath());
|
||||
$disposition = Mime::DISPOSITION_ATTACHMENT;
|
||||
break;
|
||||
|
||||
case StringAttachment::class:
|
||||
/** @var StringAttachment $attachment */
|
||||
$content = $attachment->getContent();
|
||||
$disposition = Mime::DISPOSITION_ATTACHMENT;
|
||||
break;
|
||||
|
||||
case ImageAttachment::class:
|
||||
$content = file_get_contents($attachment->getPath());
|
||||
$disposition = Mime::DISPOSITION_INLINE;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new MessageFormatException(
|
||||
sprintf('unrecognized attachment class "%s"', get_class($attachment))
|
||||
);
|
||||
}
|
||||
$part = new Part($content);
|
||||
$part->disposition = $disposition;
|
||||
$part->type = $attachment->getType();
|
||||
$part->filename = $attachment->getName();
|
||||
$part->encoding = $attachment->getEncoding();
|
||||
$attachmentParts[] = $part;
|
||||
}
|
||||
$attachmentParts = array_merge($textParts, $attachmentParts);
|
||||
$body = new MimeMessage();
|
||||
$body->setParts($attachmentParts);
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $html
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function convertHtmlToPlainText(string $html): string
|
||||
{
|
||||
return html_entity_decode(
|
||||
trim(
|
||||
strip_tags(
|
||||
preg_replace(
|
||||
'/<(head|title|style|script)[^>]*>.*?<\/\\1>/si',
|
||||
'',
|
||||
$html
|
||||
)
|
||||
)
|
||||
),
|
||||
ENT_QUOTES,
|
||||
'iso-8859-1'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Client;
|
||||
|
||||
use Xentral\Components\Mailer\Data\EmailMessage;
|
||||
use Xentral\Components\Mailer\Data\EmailRecipient;
|
||||
|
||||
interface MimeMessageFormatterInterface
|
||||
{
|
||||
/**
|
||||
* @param EmailMessage $email
|
||||
* @param EmailRecipient $sender
|
||||
* @param string|null $messageId
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function formatMessage(EmailMessage $email, EmailRecipient $sender, string $messageId = null): string;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Config;
|
||||
|
||||
final class ImapMailClientConfig implements ImapMailClientConfigInterface
|
||||
{
|
||||
/** @var string $server */
|
||||
private $server;
|
||||
|
||||
/** @var int $port */
|
||||
private $port;
|
||||
|
||||
/** @var string $user */
|
||||
private $user;
|
||||
|
||||
/** @var string $password */
|
||||
private $password;
|
||||
|
||||
/** @var string $authType */
|
||||
private $authType;
|
||||
|
||||
/** @var bool $sslEnabled */
|
||||
private $sslEnabled;
|
||||
|
||||
/** @var string $folder */
|
||||
private $folder;
|
||||
|
||||
/**
|
||||
* @param string $server
|
||||
* @param int $port
|
||||
* @param string $user
|
||||
* @param string $password
|
||||
* @param string $authType
|
||||
* @param bool $sslEnabled
|
||||
* @param string|null $folder
|
||||
*/
|
||||
public function __construct(
|
||||
string $server,
|
||||
int $port,
|
||||
string $user,
|
||||
string $password,
|
||||
string $authType = self::AUTH_BASIC,
|
||||
bool $sslEnabled = true,
|
||||
string $folder = 'INBOX'
|
||||
) {
|
||||
$this->server = $server;
|
||||
$this->port = $port;
|
||||
$this->folder = $folder;
|
||||
$this->user = $user;
|
||||
$this->password = $password;
|
||||
$this->authType = $authType;
|
||||
$this->sslEnabled = $sslEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getServer(): string
|
||||
{
|
||||
return $this->server;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getPort(): int
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUser(): string
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthType(): string
|
||||
{
|
||||
return $this->authType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isSslEnabled(): bool
|
||||
{
|
||||
return $this->sslEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getInboxFolder(): string
|
||||
{
|
||||
return $this->folder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Config;
|
||||
|
||||
interface ImapMailClientConfigInterface
|
||||
{
|
||||
/** @var string AUTH_BASIC */
|
||||
public const AUTH_BASIC = 'basic';
|
||||
|
||||
/** @var string AUTH_XOAUTH2 */
|
||||
public const AUTH_XOAUTH2 = 'xoauth2';
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getServer(): string;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getPort(): int;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUser(): string;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPassword(): string;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthType(): string;
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isSslEnabled(): bool;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getInboxFolder(): string;
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Data;
|
||||
|
||||
use Xentral\Components\MailClient\Exception\InvalidArgumentException;
|
||||
|
||||
class MailAttachmentData implements MailAttachmentInterface
|
||||
{
|
||||
/** @var string $filename */
|
||||
private $filename;
|
||||
|
||||
/** @var string $content */
|
||||
private $content;
|
||||
|
||||
/** @var string $contentType */
|
||||
private $contentType;
|
||||
|
||||
/** @var string $encoding */
|
||||
private $encoding;
|
||||
|
||||
/** @var bool $isInlineAttachment*/
|
||||
private $isInlineAttachment;
|
||||
|
||||
/** @var string|null $cid */
|
||||
private $cid;
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @param string $content
|
||||
* @param string $contentType
|
||||
* @param string $encoding
|
||||
* @param bool $isInlineAttachment
|
||||
* @param string|null $cid
|
||||
*/
|
||||
public function __construct(
|
||||
string $filename,
|
||||
string $content,
|
||||
string $contentType,
|
||||
string $encoding,
|
||||
bool $isInlineAttachment = false,
|
||||
string $cid = null
|
||||
)
|
||||
{
|
||||
$this->filename = $filename;
|
||||
$this->content = $content;
|
||||
$this->contentType = $contentType;
|
||||
$this->encoding = $encoding;
|
||||
$this->isInlineAttachment = $isInlineAttachment;
|
||||
$this->cid = $cid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MailMessagePartInterface $part
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return MailAttachmentData
|
||||
*/
|
||||
public static function fromMailMessagePart(MailMessagePartInterface $part): MailAttachmentData
|
||||
{
|
||||
$encodingHeader = $part->getHeader('content-transfer-encoding');
|
||||
if ($encodingHeader === null) {
|
||||
throw new InvalidArgumentException('missing header: "Content-Transfer-Encoding"');
|
||||
}
|
||||
$encoding = $encodingHeader->getValue();
|
||||
$dispositionHeader = $part->getHeader('content-disposition');
|
||||
if ($dispositionHeader === null) {
|
||||
throw new InvalidArgumentException('missing header: "Content-Disposition"');
|
||||
}
|
||||
$disposition = $dispositionHeader->getValue();
|
||||
if (!preg_match('/(.+);\s*filename="([^"]+)".*$/m', $disposition, $matches)) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf('unexpected header value "Content-Disposition" = %s', $disposition)
|
||||
);
|
||||
}
|
||||
$isInline = strtolower($matches[1]) === 'inline';
|
||||
$filename = $matches[2];
|
||||
$cid = null;
|
||||
$contentIdHeader = $part->getHeader('content-id');
|
||||
if ($contentIdHeader !== null) {
|
||||
$cid = $contentIdHeader->getValue();
|
||||
if (preg_match('/[<]?([^<>]+)[>]?$/', $cid, $cidMatches)) {
|
||||
$cid = $cidMatches[1];
|
||||
}
|
||||
}
|
||||
|
||||
return new self(
|
||||
$filename,
|
||||
$part->getContent(),
|
||||
$part->getContentType(),
|
||||
$encoding,
|
||||
$isInline,
|
||||
$cid
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFileName(): string
|
||||
{
|
||||
return $this->filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getContent(): string
|
||||
{
|
||||
switch ($this->encoding) {
|
||||
case 'base64':
|
||||
return base64_decode($this->content);
|
||||
|
||||
default:
|
||||
return $this->content;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getContentType(): string
|
||||
{
|
||||
return $this->contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTransferEncoding(): string
|
||||
{
|
||||
return $this->encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isInlineAttachment(): bool
|
||||
{
|
||||
return $this->isInlineAttachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCid(): ?string
|
||||
{
|
||||
return $this->cid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Data;
|
||||
|
||||
interface MailAttachmentInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFileName(): string;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getContent(): string;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getContentType(): string;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTransferEncoding(): string;
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isInlineAttachment(): bool;
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCid(): ?string;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Data;
|
||||
|
||||
final class MailBoxInfoData
|
||||
{
|
||||
/** @var int $messages */
|
||||
private $messages;
|
||||
|
||||
/** @var int $recent */
|
||||
private $recent;
|
||||
|
||||
/** @var int $uidvalidity */
|
||||
private $uidvalidity;
|
||||
|
||||
/** @var array $flags */
|
||||
private $flags;
|
||||
|
||||
/**
|
||||
* @param int $messages
|
||||
* @param int $recent
|
||||
* @param int $uidvalidity
|
||||
* @param array $flags
|
||||
*/
|
||||
public function __construct(
|
||||
int $messages,
|
||||
int $recent,
|
||||
int $uidvalidity,
|
||||
array $flags = []
|
||||
)
|
||||
{
|
||||
$this->messages = $messages;
|
||||
$this->recent = $recent;
|
||||
$this->uidvalidity = $uidvalidity;
|
||||
$this->flags = $flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int total amount of messages
|
||||
*/
|
||||
public function getMessages(): int
|
||||
{
|
||||
return $this->messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int amount of recent messages
|
||||
*/
|
||||
public function getRecentMessages(): int
|
||||
{
|
||||
return $this->recent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getUidvalidity(): int
|
||||
{
|
||||
return $this->uidvalidity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getFlags(): array
|
||||
{
|
||||
return $this->flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $flag
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasFlag(string $flag): bool
|
||||
{
|
||||
return array_key_exists($flag, $this->flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Data;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use JsonSerializable;
|
||||
use Throwable;
|
||||
use Xentral\Components\MailClient\Exception\InvalidArgumentException;
|
||||
use Xentral\Components\Mailer\Data\EmailRecipient;
|
||||
use Xentral\Components\Util\StringUtil;
|
||||
|
||||
final class MailMessageData implements MailMessageInterface, JsonSerializable
|
||||
{
|
||||
/** @var EmailRecipient $sender */
|
||||
private $sender;
|
||||
|
||||
/** @var EmailRecipient[] $recipients */
|
||||
private $recipients;
|
||||
|
||||
/** @var EmailRecipient[] $ccs */
|
||||
private $ccs;
|
||||
|
||||
/** @var array $flags */
|
||||
private $flags;
|
||||
|
||||
/** @var MailMessagePartInterface $contentPart */
|
||||
private $contentPart;
|
||||
|
||||
/** @var string $rawContent */
|
||||
private $rawContent;
|
||||
|
||||
/**
|
||||
* @param EmailRecipient $sender
|
||||
* @param array $recipients
|
||||
* @param array $ccs
|
||||
* @param array $flags
|
||||
* @param array $headers
|
||||
* @param string|null $content
|
||||
* @param array $parts
|
||||
* @param string|null $rawContent
|
||||
*/
|
||||
public function __construct(
|
||||
EmailRecipient $sender,
|
||||
array $recipients,
|
||||
array $ccs,
|
||||
array $flags,
|
||||
array $headers,
|
||||
?string $content,
|
||||
array $parts = [],
|
||||
?string $rawContent = null
|
||||
) {
|
||||
$this->sender = $sender;
|
||||
$this->recipients = $recipients;
|
||||
$this->ccs = $ccs;
|
||||
$this->flags = $flags;
|
||||
$this->rawContent = $rawContent;
|
||||
$this->contentPart = new MailMessagePartData($headers, $content, $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return MailMessageData
|
||||
*/
|
||||
public static function fromJsonArray(array $data): MailMessageData
|
||||
{
|
||||
if (!isset($data['sender'], $data['recipients'], $data['ccs'], $data['flags'])) {
|
||||
throw new InvalidArgumentException('Message data incomplete');
|
||||
}
|
||||
$sender = new EmailRecipient($data['sender']['email'], $data['sender']['name']);
|
||||
$recipients = [];
|
||||
foreach ($data['recipients'] as $recipientArray) {
|
||||
$recipients[] = new EmailRecipient($recipientArray['email'], $recipientArray['name']);
|
||||
}
|
||||
$ccs = [];
|
||||
foreach ($data['ccs'] as $ccArray) {
|
||||
$ccs[] = new EmailRecipient($ccArray['email'], $ccArray['name']);
|
||||
}
|
||||
$raw = isset($data['raw']) ? $data['raw'] : null;
|
||||
$contentPart = isset($data['content'])
|
||||
? MailMessagePartData::fromJsonArray($data['content'])
|
||||
: null;
|
||||
$message = new self(
|
||||
$sender,
|
||||
$recipients,
|
||||
$ccs,
|
||||
$data['flags'],
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
$raw
|
||||
);
|
||||
$message->contentPart = $contentPart;
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getFlags(): array
|
||||
{
|
||||
return $this->flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $flag
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasFlag(string $flag): bool
|
||||
{
|
||||
return array_key_exists($flag, $this->flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isMultipart(): bool
|
||||
{
|
||||
$contentType = $this->getContentType();
|
||||
if ($contentType === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return StringUtil::startsWith($contentType, 'multipart/');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getContentType(): string
|
||||
{
|
||||
return $this->contentPart->getContentType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MailAttachmentInterface[]
|
||||
*/
|
||||
public function getAttachments(): array
|
||||
{
|
||||
$parts = [];
|
||||
$this->findAttachmentParts($this->contentPart, $parts);
|
||||
$attachments = [];
|
||||
foreach ($parts as $part) {
|
||||
$attachments[] = MailAttachmentData::fromMailMessagePart($part);
|
||||
}
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MailMessagePartInterface $part
|
||||
* @param array $resultArray
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function findAttachmentParts(MailMessagePartInterface $part, array &$resultArray): void
|
||||
{
|
||||
try {
|
||||
$header = $part->getHeader('content-disposition');
|
||||
$split = explode(';', $header->getValue());
|
||||
if ($split[0] === 'attachment' || $split[0] === 'inline') {
|
||||
$resultArray[] = $part;
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
for ($i = 0; $i < $part->countParts(); $i++) {
|
||||
$this->findAttachmentParts($part->getPart($i), $resultArray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return MailMessageHeaderValue[]|[]
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->contentPart->getHeaders();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getContent(): ?string
|
||||
{
|
||||
return $this->contentPart->getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDecodedContent(): ?string
|
||||
{
|
||||
$this->contentPart->getDecodedContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $index
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return MailMessagePartInterface
|
||||
*/
|
||||
public function getPart(int $index): MailMessagePartInterface
|
||||
{
|
||||
return $this->contentPart->getPart($index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function countParts(): int
|
||||
{
|
||||
return $this->contentPart->countParts();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getHtmlBody(): ?string
|
||||
{
|
||||
$part = $this->findPartByContentType($this->contentPart, 'text/html');
|
||||
if ($part === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $part->getDecodedContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MailMessagePartInterface $part
|
||||
* @param string $contentType
|
||||
*
|
||||
* @return MailMessagePartInterface|null
|
||||
*/
|
||||
private function findPartByContentType(
|
||||
MailMessagePartInterface $part,
|
||||
string $contentType
|
||||
): ?MailMessagePartInterface {
|
||||
if ($part->getContentType() === $contentType) {
|
||||
return $part;
|
||||
}
|
||||
for ($i = 0; $i < $part->countParts(); $i++) {
|
||||
$subPart = $this->findPartByContentType($part->getPart($i), $contentType);
|
||||
if ($subPart !== null) {
|
||||
return $subPart;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getPlainTextBody(): ?string
|
||||
{
|
||||
$part = $this->findPartByContentType($this->contentPart, 'text/plain');
|
||||
if ($part === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $part->getDecodedContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSubject(): string
|
||||
{
|
||||
$subject = $this->getHeader('subject');
|
||||
if ($subject === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $subject->getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return MailMessageHeaderValue|null
|
||||
*/
|
||||
public function getHeader(string $name): ?MailMessageHeaderValue
|
||||
{
|
||||
return $this->contentPart->getHeader($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DateTimeInterface|null
|
||||
*/
|
||||
public function getDate(): ?DateTimeInterface
|
||||
{
|
||||
$date = $this->getHeader('date');
|
||||
if ($date === null) {
|
||||
return null;
|
||||
}
|
||||
$dateTime = DateTime::createFromFormat(DateTimeInterface::RFC2822, $date->getValue());
|
||||
if ($dateTime === false) {
|
||||
$dateTime = DateTime::createFromFormat(DateTimeInterface::RFC822, $date->getValue());
|
||||
}
|
||||
if ($dateTime === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $dateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EmailRecipient
|
||||
*/
|
||||
public function getSender(): EmailRecipient
|
||||
{
|
||||
return $this->sender;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getReplyToAddress(): string
|
||||
{
|
||||
$header = $this->getHeader('return-path');
|
||||
if ($header === null) {
|
||||
return $this->sender->getEmail();
|
||||
}
|
||||
$address = $header->getValue();
|
||||
if (preg_match('/(.*)<(.+@.+)>/', $address, $matches)) {
|
||||
$address = $matches[2];
|
||||
}
|
||||
|
||||
return $address;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EmailRecipient[]
|
||||
*/
|
||||
public function getRecipients(): array
|
||||
{
|
||||
return $this->recipients;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EmailRecipient[]
|
||||
*/
|
||||
public function getCcRecipients(): array
|
||||
{
|
||||
return $this->ccs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getRawContent(): ?string
|
||||
{
|
||||
if ($this->rawContent === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->rawContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'sender' => $this->sender,
|
||||
'recipients' => $this->recipients,
|
||||
'ccs' => $this->ccs,
|
||||
'flags' => $this->flags,
|
||||
'content' => $this->contentPart,
|
||||
'raw' => $this->rawContent,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Data;
|
||||
|
||||
interface MailMessageHeaderInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getValue(): string;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getEncoding(): string;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Data;
|
||||
|
||||
use JsonSerializable;
|
||||
use Xentral\Components\MailClient\Exception\InvalidArgumentException;
|
||||
|
||||
final class MailMessageHeaderValue implements MailMessageHeaderInterface, JsonSerializable
|
||||
{
|
||||
/** @var string $name */
|
||||
private $name;
|
||||
|
||||
/** @var string $value */
|
||||
private $value;
|
||||
|
||||
/** @var string $encoding */
|
||||
private $encoding;
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
* @param string $encoding
|
||||
*/
|
||||
public function __construct(string $name, string $value, string $encoding)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->value = $value;
|
||||
$this->encoding = $encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return MailMessageHeaderValue
|
||||
*/
|
||||
public static function fromJsonArray(array $data): MailMessageHeaderValue
|
||||
{
|
||||
if (!isset($data['name'], $data['value'], $data['encoding'])) {
|
||||
throw new InvalidArgumentException('Header incomplete');
|
||||
}
|
||||
|
||||
return new self($data['name'], $data['value'], $data['encoding']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getEncoding(): string
|
||||
{
|
||||
return $this->encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'value' => $this->value,
|
||||
'encoding' => $this->encoding,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Data;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Xentral\Components\Mailer\Data\EmailRecipient;
|
||||
|
||||
interface MailMessageInterface extends MailMessagePartInterface
|
||||
{
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getFlags(): array;
|
||||
|
||||
/**
|
||||
* @param string $flag
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasFlag(string $flag): bool;
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getHtmlBody(): ?string;
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getPlainTextBody(): ?string;
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getRawContent(): ?string;
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getSubject(): ?string;
|
||||
|
||||
/**
|
||||
* @return DateTimeInterface|null
|
||||
*/
|
||||
public function getDate(): ?DateTimeInterface;
|
||||
|
||||
/**
|
||||
* @return EmailRecipient
|
||||
*/
|
||||
public function getSender(): EmailRecipient;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getReplyToAddress(): string;
|
||||
|
||||
/**
|
||||
* @return EmailRecipient[]
|
||||
*/
|
||||
public function getRecipients(): array;
|
||||
|
||||
/**
|
||||
* @return EmailRecipient[]
|
||||
*/
|
||||
public function getCcRecipients(): array;
|
||||
|
||||
/**
|
||||
* @return MailAttachmentInterface[]
|
||||
*/
|
||||
public function getAttachments(): array;
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Data;
|
||||
|
||||
use JsonSerializable;
|
||||
use Xentral\Components\MailClient\Exception\InvalidArgumentException;
|
||||
use Xentral\Components\Util\StringUtil;
|
||||
|
||||
final class MailMessagePartData implements MailMessagePartInterface, JsonSerializable
|
||||
{
|
||||
/** @var MailMessageHeaderValue[] $headers */
|
||||
private $headers;
|
||||
|
||||
/** @var MailMessagePartInterface[] $parts */
|
||||
private $parts;
|
||||
|
||||
/** @var string|null $content */
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* @param array $headers
|
||||
* @param string|null $content
|
||||
* @param array $parts
|
||||
*/
|
||||
public function __construct(
|
||||
array $headers,
|
||||
?string $content,
|
||||
array $parts
|
||||
) {
|
||||
$headers = array_change_key_case($headers, CASE_LOWER);
|
||||
$this->headers = $headers;
|
||||
$this->content = $content;
|
||||
$this->parts = [];
|
||||
foreach ($parts as $part) {
|
||||
$this->addPart($part);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return MailMessagePartData
|
||||
*/
|
||||
public static function fromJsonArray(array $data): MailMessagePartData
|
||||
{
|
||||
if (!array_key_exists('headers', $data)) {
|
||||
throw new InvalidArgumentException('Headers required');
|
||||
}
|
||||
if (!array_key_exists('content', $data)) {
|
||||
throw new InvalidArgumentException('content required');
|
||||
}
|
||||
if (!array_key_exists('parts', $data)) {
|
||||
throw new InvalidArgumentException('Message parts required');
|
||||
}
|
||||
|
||||
$headers = [];
|
||||
foreach ($data['headers'] as $header) {
|
||||
$headerValue = MailMessageHeaderValue::fromJsonArray($header);
|
||||
$headers[$headerValue->getName()] = $headerValue;
|
||||
}
|
||||
|
||||
$part = new self($headers, $data['content'], []);
|
||||
foreach ($data['parts'] as $subPart) {
|
||||
$part->addPart(self::fromJsonArray($subPart));
|
||||
}
|
||||
|
||||
return $part;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getHeader(string $name): ?MailMessageHeaderValue
|
||||
{
|
||||
if (!isset($this->headers[strtolower($name)])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->headers[strtolower($name)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getContentType(): string
|
||||
{
|
||||
$header = $this->getHeader('content-type');
|
||||
if ($header === null) {
|
||||
return '';
|
||||
}
|
||||
$split = preg_split('/;/', $header->getValue(), -1, 0);
|
||||
|
||||
return $split[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getContent(): ?string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDecodedContent(): ?string
|
||||
{
|
||||
if ($this->content === null) {
|
||||
return null;
|
||||
}
|
||||
$encodingHeader = $this->getHeader('content-transfer-encoding');
|
||||
if ($encodingHeader === null ) {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
return $this->decode($this->content, $encodingHeader->getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getPart(int $index): MailMessagePartInterface
|
||||
{
|
||||
return $this->parts[$index];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function countParts(): int
|
||||
{
|
||||
return count($this->parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isMultipart(): bool
|
||||
{
|
||||
$contentType = $this->getContentType();
|
||||
if ($contentType === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return StringUtil::startsWith($contentType, 'multipart/');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MailMessagePartInterface $part
|
||||
*/
|
||||
private function addPart(MailMessagePartInterface $part): void
|
||||
{
|
||||
$this->parts[] = $part;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
* @param string $encoding
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function decode(string $content, string $encoding): string
|
||||
{
|
||||
switch (strtolower($encoding)) {
|
||||
case 'quoted-printable':
|
||||
return quoted_printable_decode($content);
|
||||
//no break
|
||||
|
||||
case 'base64':
|
||||
return base64_decode($content);
|
||||
//no break
|
||||
|
||||
// default includes 7bit, 8bit and binary
|
||||
default:
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'headers' => $this->headers,
|
||||
'content' => $this->content,
|
||||
'parts' => $this->parts,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Data;
|
||||
|
||||
interface MailMessagePartInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getContentType(): string;
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isMultipart(): bool;
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return MailMessageHeaderValue|null
|
||||
*/
|
||||
public function getHeader(string $name): ?MailMessageHeaderValue;
|
||||
|
||||
/**
|
||||
* @return MailMessageHeaderValue[]|[]
|
||||
*/
|
||||
public function getHeaders(): array;
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getContent(): ?string;
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDecodedContent(): ?string;
|
||||
|
||||
/**
|
||||
* @param int $index
|
||||
*
|
||||
* @return MailMessagePartInterface
|
||||
*/
|
||||
public function getPart(int $index): MailMessagePartInterface;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function countParts(): int;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Exception;
|
||||
|
||||
use RuntimeException as SplRuntimeException;
|
||||
|
||||
final class ClientConnectionException extends SplRuntimeException implements MailClientExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Exception;
|
||||
|
||||
use RuntimeException as SplRuntimeException;
|
||||
|
||||
final class FolderNotFoundException extends SplRuntimeException implements MailClientExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Exception;
|
||||
|
||||
use InvalidArgumentException as SplInvalidArgumentException;
|
||||
|
||||
final class InvalidArgumentException extends SplInvalidArgumentException implements MailClientExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Exception;
|
||||
|
||||
use RuntimeException as SplRuntimeException;
|
||||
|
||||
final class LoginException extends SplRuntimeException implements MailClientExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Exception;
|
||||
|
||||
use Xentral\Core\Exception\ComponentExceptionInterface;
|
||||
|
||||
interface MailClientExceptionInterface extends ComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Exception;
|
||||
|
||||
use RuntimeException as SplRuntimeException;
|
||||
|
||||
final class MessageFormatException extends SplRuntimeException implements MailClientExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Exception;
|
||||
|
||||
use RuntimeException as SplRuntimeException;
|
||||
|
||||
final class MessageNotFoundException extends SplRuntimeException implements MailClientExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Exception;
|
||||
|
||||
use RuntimeException as SplRuntimeException;
|
||||
|
||||
final class OAuthException extends SplRuntimeException implements MailClientExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Exception;
|
||||
|
||||
use RuntimeException as SplRuntimeException;
|
||||
|
||||
final class ProtocolException extends SplRuntimeException implements MailClientExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient\Exception;
|
||||
|
||||
use RuntimeException as SplRuntimeException;
|
||||
|
||||
final class RuntimeException extends SplRuntimeException implements MailClientExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\MailClient;
|
||||
|
||||
use Xentral\Components\MailClient\Client\ImapMailClient;
|
||||
use Xentral\Components\MailClient\Config\ImapMailClientConfigInterface;
|
||||
|
||||
final class MailClientFactory
|
||||
{
|
||||
/**
|
||||
* @param ImapMailClientConfigInterface $config
|
||||
*
|
||||
* @return ImapMailClient
|
||||
*/
|
||||
public function createImapClient(ImapMailClientConfigInterface $config): ImapMailClient
|
||||
{
|
||||
return new ImapMailClient($config);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user