Migrated away from PHP Mail Parser to the new WebKlex PHP IMAP Mail Parser this will open the way to support OAUTH2 for Mail servers such as Microsoft 365 and Google Workspaces

This commit is contained in:
johnnyq
2024-06-12 15:39:52 -04:00
parent d64a7ce31e
commit 779527cf6a
218 changed files with 14781 additions and 2722 deletions

View File

@@ -0,0 +1,90 @@
<?php
/*
* File: Address.php
* Category: -
* Author: M. Goldenbaum
* Created: 01.01.21 21:17
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP;
/**
* Class Address
*
* @package Webklex\PHPIMAP
*/
class Address {
/**
* Address attributes
* @var string $personal
* @var string $mailbox
* @var string $host
* @var string $mail
* @var string $full
*/
public string $personal = "";
public string $mailbox = "";
public string $host = "";
public string $mail = "";
public string $full = "";
/**
* Address constructor.
* @param object $object
*/
public function __construct(object $object) {
if (property_exists($object, "personal")){ $this->personal = $object->personal ?? ''; }
if (property_exists($object, "mailbox")){ $this->mailbox = $object->mailbox ?? ''; }
if (property_exists($object, "host")){ $this->host = $object->host ?? ''; }
if (property_exists($object, "mail")){ $this->mail = $object->mail ?? ''; }
if (property_exists($object, "full")){ $this->full = $object->full ?? ''; }
}
/**
* Return the stringified address
*
* @return string
*/
public function __toString() {
return $this->full ?: "";
}
/**
* Return the serialized address
*
* @return array
*/
public function __serialize(){
return [
"personal" => $this->personal,
"mailbox" => $this->mailbox,
"host" => $this->host,
"mail" => $this->mail,
"full" => $this->full,
];
}
/**
* Convert instance to array
*
* @return array
*/
public function toArray(): array {
return $this->__serialize();
}
/**
* Return the stringified attribute
*
* @return string
*/
public function toString(): string {
return $this->__toString();
}
}

View File

@@ -0,0 +1,455 @@
<?php
/*
* File: Attachment.php
* Category: -
* Author: M. Goldenbaum
* Created: 16.03.18 19:37
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP;
use Illuminate\Support\Str;
use Webklex\PHPIMAP\Exceptions\MaskNotFoundException;
use Webklex\PHPIMAP\Exceptions\MethodNotFoundException;
use Webklex\PHPIMAP\Support\Masks\AttachmentMask;
/**
* Class Attachment
*
* @package Webklex\PHPIMAP
*
* @property integer part_number
* @property integer size
* @property string content
* @property string type
* @property string content_type
* @property string id
* @property string hash
* @property string name
* @property string description
* @property string filename
* @property ?string disposition
* @property string img_src
*
* @method integer getPartNumber()
* @method integer setPartNumber(integer $part_number)
* @method string getContent()
* @method string setContent(string $content)
* @method string getType()
* @method string setType(string $type)
* @method string getContentType()
* @method string setContentType(string $content_type)
* @method string getId()
* @method string setId(string $id)
* @method string getHash()
* @method string setHash(string $hash)
* @method string getSize()
* @method string setSize(integer $size)
* @method string getName()
* @method string getDisposition()
* @method string setDisposition(string $disposition)
* @method string setImgSrc(string $img_src)
*/
class Attachment {
/**
* @var Message $message
*/
protected Message $message;
/**
* Used config
*
* @var Config $config
*/
protected Config $config;
/**
* Attachment options
*
* @var array $options
*/
protected array $options = [];
/** @var Part $part */
protected Part $part;
/**
* Attribute holder
*
* @var array $attributes
*/
protected array $attributes = [
'content' => null,
'hash' => null,
'type' => null,
'part_number' => 0,
'content_type' => null,
'id' => null,
'name' => null,
'filename' => null,
'description' => null,
'disposition' => null,
'img_src' => null,
'size' => null,
];
/**
* Default mask
*
* @var string $mask
*/
protected string $mask = AttachmentMask::class;
/**
* Attachment constructor.
* @param Message $message
* @param Part $part
*/
public function __construct(Message $message, Part $part) {
$this->message = $message;
$this->config = $this->message->getConfig();
$this->options = $this->config->get('options');
$this->part = $part;
$this->part_number = $part->part_number;
if ($this->message->getClient()) {
$default_mask = $this->message->getClient()?->getDefaultAttachmentMask();
if ($default_mask != null) {
$this->mask = $default_mask;
}
} else {
$default_mask = $this->config->getMask("attachment");
if ($default_mask != "") {
$this->mask = $default_mask;
}
}
$this->findType();
$this->fetch();
}
/**
* Call dynamic attribute setter and getter methods
* @param string $method
* @param array $arguments
*
* @return mixed
* @throws MethodNotFoundException
*/
public function __call(string $method, array $arguments) {
if (strtolower(substr($method, 0, 3)) === 'get') {
$name = Str::snake(substr($method, 3));
if (isset($this->attributes[$name])) {
return $this->attributes[$name];
}
return null;
} elseif (strtolower(substr($method, 0, 3)) === 'set') {
$name = Str::snake(substr($method, 3));
$this->attributes[$name] = array_pop($arguments);
return $this->attributes[$name];
}
throw new MethodNotFoundException("Method " . self::class . '::' . $method . '() is not supported');
}
/**
* Magic setter
* @param $name
* @param $value
*
* @return mixed
*/
public function __set($name, $value) {
$this->attributes[$name] = $value;
return $this->attributes[$name];
}
/**
* magic getter
* @param $name
*
* @return mixed|null
*/
public function __get($name) {
if (isset($this->attributes[$name])) {
return $this->attributes[$name];
}
return null;
}
/**
* Determine the structure type
*/
protected function findType(): void {
$this->type = match ($this->part->type) {
IMAP::ATTACHMENT_TYPE_MESSAGE => 'message',
IMAP::ATTACHMENT_TYPE_APPLICATION => 'application',
IMAP::ATTACHMENT_TYPE_AUDIO => 'audio',
IMAP::ATTACHMENT_TYPE_IMAGE => 'image',
IMAP::ATTACHMENT_TYPE_VIDEO => 'video',
IMAP::ATTACHMENT_TYPE_MODEL => 'model',
IMAP::ATTACHMENT_TYPE_TEXT => 'text',
IMAP::ATTACHMENT_TYPE_MULTIPART => 'multipart',
default => 'other',
};
}
/**
* Fetch the given attachment
*/
protected function fetch(): void {
$content = $this->part->content;
$this->content_type = $this->part->content_type;
$this->content = $this->message->decodeString($content, $this->part->encoding);
// Create a hash of the raw part - this can be used to identify the attachment in the message context. However,
// it is not guaranteed to be unique and collisions are possible.
// Some additional online resources:
// - https://en.wikipedia.org/wiki/Hash_collision
// - https://www.php.net/manual/en/function.hash.php
// - https://php.watch/articles/php-hash-benchmark
// Benchmark speeds:
// -xxh3 ~15.19(GB/s) (requires php-xxhash extension or >= php8.1)
// -crc32c ~14.12(GB/s)
// -sha256 ~0.25(GB/s)
// xxh3 would be nice to use, because of its extra speed and 32 instead of 8 bytes, but it is not compatible with
// php < 8.1. crc32c is the next fastest and is compatible with php >= 5.1. sha256 is the slowest, but is compatible
// with php >= 5.1 and is the most likely to be unique. crc32c is the best compromise between speed and uniqueness.
// Unique enough for our purposes, but not so slow that it could be a bottleneck.
$this->hash = hash("crc32c", $this->part->getHeader()->raw."\r\n\r\n".$this->part->content);
if (($id = $this->part->id) !== null) {
$this->id = str_replace(['<', '>'], '', $id);
}else {
$this->id = $this->hash;
}
$this->size = $this->part->bytes;
$this->disposition = $this->part->disposition;
if (($filename = $this->part->filename) !== null) {
$this->filename = $this->decodeName($filename);
}
if (($description = $this->part->description) !== null) {
$this->description = $this->part->getHeader()->decode($description);
}
if (($name = $this->part->name) !== null) {
$this->name = $this->decodeName($name);
}
if (IMAP::ATTACHMENT_TYPE_MESSAGE == $this->part->type) {
if ($this->part->ifdescription) {
if (!$this->name) {
$this->name = $this->part->description;
}
} else if (!$this->name) {
$this->name = $this->part->subtype;
}
}
$this->attributes = array_merge($this->part->getHeader()->getAttributes(), $this->attributes);
if (!$this->filename) {
$this->filename = $this->hash;
}
if (!$this->name && $this->filename != "") {
$this->name = $this->filename;
}
}
/**
* Save the attachment content to your filesystem
* @param string $path
* @param string|null $filename
*
* @return boolean
*/
public function save(string $path, ?string $filename = null): bool {
$filename = $filename ? $this->decodeName($filename) : $this->filename;
return file_put_contents($path . DIRECTORY_SEPARATOR . $filename, $this->getContent()) !== false;
}
/**
* Decode a given name
* @param string|null $name
*
* @return string
*/
public function decodeName(?string $name): string {
if ($name !== null) {
if (str_contains($name, "''")) {
$parts = explode("''", $name);
if (EncodingAliases::has($parts[0])) {
$name = implode("''", array_slice($parts, 1));
}
}
$decoder = $this->options['decoder']['message'];
if (preg_match('/=\?([^?]+)\?(Q|B)\?(.+)\?=/i', $name, $matches)) {
$name = $this->part->getHeader()->decode($name);
} elseif ($decoder === 'utf-8' && extension_loaded('imap')) {
$name = \imap_utf8($name);
}
// check if $name is url encoded
if (preg_match('/%[0-9A-F]{2}/i', $name)) {
$name = urldecode($name);
}
// sanitize $name
// order of '..' is important
return str_replace(['\\', '/', chr(0), ':', '..'], '', $name);
}
return "";
}
/**
* Get the attachment mime type
*
* @return string|null
*/
public function getMimeType(): ?string {
return (new \finfo())->buffer($this->getContent(), FILEINFO_MIME_TYPE);
}
/**
* Try to guess the attachment file extension
*
* @return string|null
*/
public function getExtension(): ?string {
$extension = null;
$guesser = "\Symfony\Component\Mime\MimeTypes";
if (class_exists($guesser) !== false) {
/** @var Symfony\Component\Mime\MimeTypes $guesser */
$extensions = $guesser::getDefault()->getExtensions($this->getMimeType());
$extension = $extensions[0] ?? null;
}
if ($extension === null) {
$deprecated_guesser = "\Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser";
if (class_exists($deprecated_guesser) !== false) {
/** @var \Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser $deprecated_guesser */
$extension = $deprecated_guesser::getInstance()->guess($this->getMimeType());
}
}
if ($extension === null) {
$parts = explode(".", $this->filename);
$extension = count($parts) > 1 ? end($parts) : null;
}
if ($extension === null) {
$parts = explode(".", $this->name);
$extension = count($parts) > 1 ? end($parts) : null;
}
return $extension;
}
/**
* Get all attributes
*
* @return array
*/
public function getAttributes(): array {
return $this->attributes;
}
/**
* @return Message
*/
public function getMessage(): Message {
return $this->message;
}
/**
* Set the default mask
* @param $mask
*
* @return $this
*/
public function setMask($mask): Attachment {
if (class_exists($mask)) {
$this->mask = $mask;
}
return $this;
}
/**
* Get the used default mask
*
* @return string
*/
public function getMask(): string {
return $this->mask;
}
/**
* Get the attachment options
* @return array
*/
public function getOptions(): array {
return $this->options;
}
/**
* Set the attachment options
* @param array $options
*
* @return $this
*/
public function setOptions(array $options): Attachment {
$this->options = $options;
return $this;
}
/**
* Get the used config
*
* @return Config
*/
public function getConfig(): Config {
return $this->config;
}
/**
* Set the used config
* @param Config $config
*
* @return $this
*/
public function setConfig(Config $config): Attachment {
$this->config = $config;
return $this;
}
/**
* Get a masked instance by providing a mask name
* @param string|null $mask
*
* @return mixed
* @throws MaskNotFoundException
*/
public function mask(string $mask = null): mixed {
$mask = $mask !== null ? $mask : $this->mask;
if (class_exists($mask)) {
return new $mask($this);
}
throw new MaskNotFoundException("Unknown mask provided: " . $mask);
}
}

View File

@@ -0,0 +1,325 @@
<?php
/*
* File: Attribute.php
* Category: -
* Author: M. Goldenbaum
* Created: 01.01.21 20:17
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP;
use ArrayAccess;
use Carbon\Carbon;
/**
* Class Attribute
*
* @package Webklex\PHPIMAP
*/
class Attribute implements ArrayAccess {
/** @var string $name */
protected string $name;
/**
* Value holder
*
* @var array $values
*/
protected array $values = [];
/**
* Attribute constructor.
* @param string $name
* @param mixed|null $value
*/
public function __construct(string $name, mixed $value = null) {
$this->setName($name);
$this->add($value);
}
/**
* Handle class invocation calls
*
* @return array|string
*/
public function __invoke(): array|string {
if ($this->count() > 1) {
return $this->toArray();
}
return $this->toString();
}
/**
* Return the serialized address
*
* @return array
*/
public function __serialize(){
return $this->values;
}
/**
* Return the stringified attribute
*
* @return string
*/
public function __toString() {
return implode(", ", $this->values);
}
/**
* Return the stringified attribute
*
* @return string
*/
public function toString(): string {
return $this->__toString();
}
/**
* Convert instance to array
*
* @return array
*/
public function toArray(): array {
return $this->__serialize();
}
/**
* Convert first value to a date object
*
* @return Carbon
*/
public function toDate(): Carbon {
$date = $this->first();
if ($date instanceof Carbon) return $date;
return Carbon::parse($date);
}
/**
* Determine if a value exists at a given key.
*
* @param int|string $key
* @return bool
*/
public function has(mixed $key = 0): bool {
return array_key_exists($key, $this->values);
}
/**
* Determine if a value exists at a given key.
*
* @param int|string $key
* @return bool
*/
public function exist(mixed $key = 0): bool {
return $this->has($key);
}
/**
* Check if the attribute contains the given value
* @param mixed $value
*
* @return bool
*/
public function contains(mixed $value): bool {
return in_array($value, $this->values, true);
}
/**
* Get a value by a given key.
*
* @param int|string $key
* @return mixed
*/
public function get(int|string $key = 0): mixed {
return $this->values[$key] ?? null;
}
/**
* Set the value by a given key.
*
* @param mixed $key
* @param mixed $value
* @return Attribute
*/
public function set(mixed $value, mixed $key = 0): Attribute {
if (is_null($key)) {
$this->values[] = $value;
} else {
$this->values[$key] = $value;
}
return $this;
}
/**
* Unset a value by a given key.
*
* @param int|string $key
* @return Attribute
*/
public function remove(int|string $key = 0): Attribute {
if (isset($this->values[$key])) {
unset($this->values[$key]);
}
return $this;
}
/**
* Add one or more values to the attribute
* @param array|mixed $value
* @param boolean $strict
*
* @return Attribute
*/
public function add(mixed $value, bool $strict = false): Attribute {
if (is_array($value)) {
return $this->merge($value, $strict);
}elseif ($value !== null) {
$this->attach($value, $strict);
}
return $this;
}
/**
* Merge a given array of values with the current values array
* @param array $values
* @param boolean $strict
*
* @return Attribute
*/
public function merge(array $values, bool $strict = false): Attribute {
foreach ($values as $value) {
$this->attach($value, $strict);
}
return $this;
}
/**
* Attach a given value to the current value array
* @param $value
* @param bool $strict
* @return Attribute
*/
public function attach($value, bool $strict = false): Attribute {
if ($strict === true) {
if ($this->contains($value) === false) {
$this->values[] = $value;
}
}else{
$this->values[] = $value;
}
return $this;
}
/**
* Set the attribute name
* @param $name
*
* @return Attribute
*/
public function setName($name): Attribute {
$this->name = $name;
return $this;
}
/**
* Get the attribute name
*
* @return string
*/
public function getName(): string {
return $this->name;
}
/**
* Get all values
*
* @return array
*/
public function all(): array {
reset($this->values);
return $this->values;
}
/**
* Get the first value if possible
*
* @return mixed|null
*/
public function first(): mixed {
return reset($this->values);
}
/**
* Get the last value if possible
*
* @return mixed|null
*/
public function last(): mixed {
return end($this->values);
}
/**
* Get the number of values
*
* @return int
*/
public function count(): int {
return count($this->values);
}
/**
* @see ArrayAccess::offsetExists
* @param mixed $offset
* @return bool
*/
public function offsetExists(mixed $offset): bool {
return $this->has($offset);
}
/**
* @see ArrayAccess::offsetGet
* @param mixed $offset
* @return mixed
*/
public function offsetGet(mixed $offset): mixed {
return $this->get($offset);
}
/**
* @see ArrayAccess::offsetSet
* @param mixed $offset
* @param mixed $value
* @return void
*/
public function offsetSet(mixed $offset, mixed $value): void {
$this->set($value, $offset);
}
/**
* @see ArrayAccess::offsetUnset
* @param mixed $offset
* @return void
*/
public function offsetUnset(mixed $offset): void {
$this->remove($offset);
}
/**
* @param callable $callback
* @return array
*/
public function map(callable $callback): array {
return array_map($callback, $this->values);
}
}

951
plugins/php-imap/src/Client.php Executable file
View File

@@ -0,0 +1,951 @@
<?php
/*
* File: Client.php
* Category: -
* Author: M. Goldenbaum
* Created: 19.01.17 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP;
use ErrorException;
use Webklex\PHPIMAP\Connection\Protocols\ImapProtocol;
use Webklex\PHPIMAP\Connection\Protocols\LegacyProtocol;
use Webklex\PHPIMAP\Connection\Protocols\ProtocolInterface;
use Webklex\PHPIMAP\Exceptions\AuthFailedException;
use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
use Webklex\PHPIMAP\Exceptions\EventNotFoundException;
use Webklex\PHPIMAP\Exceptions\FolderFetchingException;
use Webklex\PHPIMAP\Exceptions\ImapBadRequestException;
use Webklex\PHPIMAP\Exceptions\ImapServerErrorException;
use Webklex\PHPIMAP\Exceptions\MaskNotFoundException;
use Webklex\PHPIMAP\Exceptions\ProtocolNotSupportedException;
use Webklex\PHPIMAP\Exceptions\ResponseException;
use Webklex\PHPIMAP\Exceptions\RuntimeException;
use Webklex\PHPIMAP\Support\FolderCollection;
use Webklex\PHPIMAP\Support\Masks\AttachmentMask;
use Webklex\PHPIMAP\Support\Masks\MessageMask;
use Webklex\PHPIMAP\Traits\HasEvents;
/**
* Class Client
*
* @package Webklex\PHPIMAP
*/
class Client {
use HasEvents;
/**
* Connection resource
*
* @var ?ProtocolInterface
*/
public ?ProtocolInterface $connection = null;
/**
* Client configuration
*
* @var Config
*/
protected Config $config;
/**
* Server hostname.
*
* @var string
*/
public string $host;
/**
* Server port.
*
* @var int
*/
public int $port;
/**
* Service protocol.
*
* @var string
*/
public string $protocol;
/**
* Server encryption.
* Supported: none, ssl, tls, starttls or notls.
*
* @var string
*/
public string $encryption;
/**
* If server has to validate cert.
*
* @var bool
*/
public bool $validate_cert = true;
/**
* Proxy settings
* @var array
*/
protected array $proxy = [
'socket' => null,
'request_fulluri' => false,
'username' => null,
'password' => null,
];
/**
* Connection timeout
* @var int $timeout
*/
public int $timeout;
/**
* Account username
*
* @var string
*/
public string $username;
/**
* Account password.
*
* @var string
*/
public string $password;
/**
* Additional data fetched from the server.
*
* @var array
*/
public array $extensions;
/**
* Account authentication method.
*
* @var ?string
*/
public ?string $authentication;
/**
* Active folder path.
*
* @var ?string
*/
protected ?string $active_folder = null;
/**
* Default message mask
*
* @var string $default_message_mask
*/
protected string $default_message_mask = MessageMask::class;
/**
* Default attachment mask
*
* @var string $default_attachment_mask
*/
protected string $default_attachment_mask = AttachmentMask::class;
/**
* Used default account values
*
* @var array $default_account_config
*/
protected array $default_account_config = [
'host' => 'localhost',
'port' => 993,
'protocol' => 'imap',
'encryption' => 'ssl',
'validate_cert' => true,
'username' => '',
'password' => '',
'authentication' => null,
"extensions" => [],
'proxy' => [
'socket' => null,
'request_fulluri' => false,
'username' => null,
'password' => null,
],
"timeout" => 30
];
/**
* Client constructor.
* @param Config $config
*
* @throws MaskNotFoundException
*/
public function __construct(Config $config) {
$this->setConfig($config);
$this->setMaskFromConfig();
$this->setEventsFromConfig();
}
/**
* Client destructor
*
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
*/
public function __destruct() {
$this->disconnect();
}
/**
* Clone the current Client instance
*
* @return Client
* @throws MaskNotFoundException
*/
public function clone(): Client {
$client = new self($this->config);
$client->events = $this->events;
$client->timeout = $this->timeout;
$client->active_folder = $this->active_folder;
$client->default_account_config = $this->default_account_config;
$config = $this->getAccountConfig();
foreach($config as $key => $value) {
$client->setAccountConfig($key, $this->default_account_config);
}
$client->default_message_mask = $this->default_message_mask;
$client->default_attachment_mask = $this->default_message_mask;
return $client;
}
/**
* Set the Client configuration
* @param Config $config
*
* @return self
*/
public function setConfig(Config $config): Client {
$this->config = $config;
$default_account = $this->config->get('default');
$default_config = $this->config->get("accounts.$default_account");
foreach ($this->default_account_config as $key => $value) {
$this->setAccountConfig($key, $default_config);
}
return $this;
}
/**
* Get the current config
*
* @return Config
*/
public function getConfig(): Config {
return $this->config;
}
/**
* Set a specific account config
* @param string $key
* @param array $default_config
*/
private function setAccountConfig(string $key, array $default_config): void {
$value = $this->default_account_config[$key];
if(isset($default_config[$key])) {
$value = $default_config[$key];
}
$this->$key = $value;
}
/**
* Get the current account config
*
* @return array
*/
public function getAccountConfig(): array {
$config = [];
foreach($this->default_account_config as $key => $value) {
if(property_exists($this, $key)) {
$config[$key] = $this->$key;
}
}
return $config;
}
/**
* Look for a possible events in any available config
*/
protected function setEventsFromConfig(): void {
$this->events = $this->config->get("events");
if(isset($config['events'])){
foreach($config['events'] as $section => $events) {
$this->events[$section] = array_merge($this->events[$section], $events);
}
}
}
/**
* Look for a possible mask in any available config
*
* @throws MaskNotFoundException
*/
protected function setMaskFromConfig(): void {
$masks = $this->config->get("masks");
if(isset($masks)){
if(isset($masks['message'])) {
if(class_exists($masks['message'])) {
$this->default_message_mask = $masks['message'];
}else{
throw new MaskNotFoundException("Unknown mask provided: ".$masks['message']);
}
}else{
$default_mask = $this->config->getMask("message");
if($default_mask != ""){
$this->default_message_mask = $default_mask;
}else{
throw new MaskNotFoundException("Unknown message mask provided");
}
}
if(isset($masks['attachment'])) {
if(class_exists($masks['attachment'])) {
$this->default_attachment_mask = $masks['attachment'];
}else{
throw new MaskNotFoundException("Unknown mask provided: ". $masks['attachment']);
}
}else{
$default_mask = $this->config->getMask("attachment");
if($default_mask != ""){
$this->default_attachment_mask = $default_mask;
}else{
throw new MaskNotFoundException("Unknown attachment mask provided");
}
}
}else{
$default_mask = $this->config->getMask("message");
if($default_mask != ""){
$this->default_message_mask = $default_mask;
}else{
throw new MaskNotFoundException("Unknown message mask provided");
}
$default_mask = $this->config->getMask("attachment");
if($default_mask != ""){
$this->default_attachment_mask = $default_mask;
}else{
throw new MaskNotFoundException("Unknown attachment mask provided");
}
}
}
/**
* Get the current imap resource
*
* @return ProtocolInterface
* @throws ConnectionFailedException
* @throws AuthFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws ResponseException
*/
public function getConnection(): ProtocolInterface {
$this->checkConnection();
return $this->connection;
}
/**
* Determine if connection was established.
*
* @return bool
*/
public function isConnected(): bool {
return $this->connection && $this->connection->connected();
}
/**
* Determine if connection was established and connect if not.
* Returns true if the connection was closed and has been reopened.
*
* @throws ConnectionFailedException
* @throws AuthFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws ResponseException
*/
public function checkConnection(): bool {
try {
if (!$this->isConnected()) {
$this->connect();
return true;
}
} catch (\Throwable) {
$this->connect();
}
return false;
}
/**
* Force the connection to reconnect
*
* @throws ConnectionFailedException
* @throws AuthFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws ResponseException
*/
public function reconnect(): void {
if ($this->isConnected()) {
$this->disconnect();
}
$this->connect();
}
/**
* Connect to server.
*
* @return $this
* @throws ConnectionFailedException
* @throws AuthFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws ResponseException
*/
public function connect(): Client {
$this->disconnect();
$protocol = strtolower($this->protocol);
if (in_array($protocol, ['imap', 'imap4', 'imap4rev1'])) {
$this->connection = new ImapProtocol($this->config, $this->validate_cert, $this->encryption);
$this->connection->setConnectionTimeout($this->timeout);
$this->connection->setProxy($this->proxy);
}else{
if (extension_loaded('imap') === false) {
throw new ConnectionFailedException("connection setup failed", 0, new ProtocolNotSupportedException($protocol." is an unsupported protocol"));
}
$this->connection = new LegacyProtocol($this->config, $this->validate_cert, $this->encryption);
if (str_starts_with($protocol, "legacy-")) {
$protocol = substr($protocol, 7);
}
$this->connection->setProtocol($protocol);
}
if ($this->config->get('options.debug')) {
$this->connection->enableDebug();
}
if (!$this->config->get('options.uid_cache')) {
$this->connection->disableUidCache();
}
try {
$this->connection->connect($this->host, $this->port);
} catch (ErrorException|RuntimeException $e) {
throw new ConnectionFailedException("connection setup failed", 0, $e);
}
$this->authenticate();
return $this;
}
/**
* Authenticate the current session
*
* @throws AuthFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws ResponseException
*/
protected function authenticate(): void {
if ($this->authentication == "oauth") {
if (!$this->connection->authenticate($this->username, $this->password)->validatedData()) {
throw new AuthFailedException();
}
} elseif (!$this->connection->login($this->username, $this->password)->validatedData()) {
throw new AuthFailedException();
}
}
/**
* Disconnect from server.
*
* @return $this
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
*/
public function disconnect(): Client {
if ($this->isConnected()) {
$this->connection->logout();
}
$this->active_folder = null;
return $this;
}
/**
* Get a folder instance by a folder name
* @param string $folder_name
* @param string|null $delimiter
* @param bool $utf7
* @return Folder|null
* @throws AuthFailedException
* @throws ConnectionFailedException
* @throws FolderFetchingException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws ResponseException
* @throws RuntimeException
*/
public function getFolder(string $folder_name, ?string $delimiter = null, bool $utf7 = false): ?Folder {
// Set delimiter to false to force selection via getFolderByName (maybe useful for uncommon folder names)
$delimiter = is_null($delimiter) ? $this->config->get('options.delimiter', "/") : $delimiter;
if (str_contains($folder_name, (string)$delimiter)) {
return $this->getFolderByPath($folder_name, $utf7);
}
return $this->getFolderByName($folder_name);
}
/**
* Get a folder instance by a folder name
* @param $folder_name
* @param bool $soft_fail If true, it will return null instead of throwing an exception
*
* @return Folder|null
* @throws FolderFetchingException
* @throws ConnectionFailedException
* @throws AuthFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws ResponseException
*/
public function getFolderByName($folder_name, bool $soft_fail = false): ?Folder {
return $this->getFolders(false, null, $soft_fail)->where("name", $folder_name)->first();
}
/**
* Get a folder instance by a folder path
* @param $folder_path
* @param bool $utf7
* @param bool $soft_fail If true, it will return null instead of throwing an exception
*
* @return Folder|null
* @throws AuthFailedException
* @throws ConnectionFailedException
* @throws FolderFetchingException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws ResponseException
* @throws RuntimeException
*/
public function getFolderByPath($folder_path, bool $utf7 = false, bool $soft_fail = false): ?Folder {
if (!$utf7) $folder_path = EncodingAliases::convert($folder_path, "utf-8", "utf7-imap");
return $this->getFolders(false, null, $soft_fail)->where("path", $folder_path)->first();
}
/**
* Get folders list.
* If hierarchical order is set to true, it will make a tree of folders, otherwise it will return flat array.
*
* @param boolean $hierarchical
* @param string|null $parent_folder
* @param bool $soft_fail If true, it will return an empty collection instead of throwing an exception
*
* @return FolderCollection
* @throws AuthFailedException
* @throws ConnectionFailedException
* @throws FolderFetchingException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws ResponseException
* @throws RuntimeException
*/
public function getFolders(bool $hierarchical = true, string $parent_folder = null, bool $soft_fail = false): FolderCollection {
$this->checkConnection();
$folders = FolderCollection::make([]);
$pattern = $parent_folder.($hierarchical ? '%' : '*');
$items = $this->connection->folders('', $pattern)->validatedData();
if(!empty($items)){
foreach ($items as $folder_name => $item) {
$folder = new Folder($this, $folder_name, $item["delimiter"], $item["flags"]);
if ($hierarchical && $folder->hasChildren()) {
$pattern = $folder->full_name.$folder->delimiter.'%';
$children = $this->getFolders(true, $pattern, $soft_fail);
$folder->setChildren($children);
}
$folders->push($folder);
}
return $folders;
}else if (!$soft_fail){
throw new FolderFetchingException("failed to fetch any folders");
}
return $folders;
}
/**
* Get folders list.
* If hierarchical order is set to true, it will make a tree of folders, otherwise it will return flat array.
*
* @param boolean $hierarchical
* @param string|null $parent_folder
* @param bool $soft_fail If true, it will return an empty collection instead of throwing an exception
*
* @return FolderCollection
* @throws FolderFetchingException
* @throws ConnectionFailedException
* @throws AuthFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws ResponseException
*/
public function getFoldersWithStatus(bool $hierarchical = true, string $parent_folder = null, bool $soft_fail = false): FolderCollection {
$this->checkConnection();
$folders = FolderCollection::make([]);
$pattern = $parent_folder.($hierarchical ? '%' : '*');
$items = $this->connection->folders('', $pattern)->validatedData();
if(!empty($items)){
foreach ($items as $folder_name => $item) {
$folder = new Folder($this, $folder_name, $item["delimiter"], $item["flags"]);
if ($hierarchical && $folder->hasChildren()) {
$pattern = $folder->full_name.$folder->delimiter.'%';
$children = $this->getFoldersWithStatus(true, $pattern, $soft_fail);
$folder->setChildren($children);
}
$folder->loadStatus();
$folders->push($folder);
}
return $folders;
}else if (!$soft_fail){
throw new FolderFetchingException("failed to fetch any folders");
}
return $folders;
}
/**
* Open a given folder.
* @param string $folder_path
* @param boolean $force_select
*
* @return array
* @throws ConnectionFailedException
* @throws AuthFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws ResponseException
*/
public function openFolder(string $folder_path, bool $force_select = false): array {
if ($this->active_folder == $folder_path && $this->isConnected() && $force_select === false) {
return [];
}
$this->checkConnection();
$this->active_folder = $folder_path;
return $this->connection->selectFolder($folder_path)->validatedData();
}
/**
* Set active folder
* @param string|null $folder_path
*
* @return void
*/
public function setActiveFolder(?string $folder_path = null): void {
$this->active_folder = $folder_path;
}
/**
* Get active folder
*
* @return string|null
*/
public function getActiveFolder(): ?string {
return $this->active_folder;
}
/**
* Create a new Folder
* @param string $folder_path
* @param boolean $expunge
* @param bool $utf7
* @return Folder
* @throws AuthFailedException
* @throws ConnectionFailedException
* @throws EventNotFoundException
* @throws FolderFetchingException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws ResponseException
* @throws RuntimeException
*/
public function createFolder(string $folder_path, bool $expunge = true, bool $utf7 = false): Folder {
$this->checkConnection();
if (!$utf7) $folder_path = EncodingAliases::convert($folder_path, "utf-8", "UTF7-IMAP");
$status = $this->connection->createFolder($folder_path)->validatedData();
if($expunge) $this->expunge();
$folder = $this->getFolderByPath($folder_path, true);
if($status && $folder) {
$event = $this->getEvent("folder", "new");
$event::dispatch($folder);
}
return $folder;
}
/**
* Delete a given folder
* @param string $folder_path
* @param boolean $expunge
*
* @return array
* @throws AuthFailedException
* @throws ConnectionFailedException
* @throws EventNotFoundException
* @throws FolderFetchingException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws ResponseException
*/
public function deleteFolder(string $folder_path, bool $expunge = true): array {
$this->checkConnection();
$folder = $this->getFolderByPath($folder_path);
if ($this->active_folder == $folder->path){
$this->active_folder = null;
}
$status = $this->getConnection()->deleteFolder($folder->path)->validatedData();
if ($expunge) $this->expunge();
$event = $this->getEvent("folder", "deleted");
$event::dispatch($folder);
return $status;
}
/**
* Check a given folder
* @param string $folder_path
*
* @return array
* @throws ConnectionFailedException
* @throws AuthFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws ResponseException
*/
public function checkFolder(string $folder_path): array {
$this->checkConnection();
return $this->connection->examineFolder($folder_path)->validatedData();
}
/**
* Get the current active folder
*
* @return string
*/
public function getFolderPath(): string {
return $this->active_folder;
}
/**
* Exchange identification information
* Ref.: https://datatracker.ietf.org/doc/html/rfc2971
*
* @param array|null $ids
* @return array
*
* @throws ConnectionFailedException
* @throws AuthFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws ResponseException
*/
public function Id(array $ids = null): array {
$this->checkConnection();
return $this->connection->ID($ids)->validatedData();
}
/**
* Retrieve the quota level settings, and usage statics per mailbox
*
* @return array
* @throws ConnectionFailedException
* @throws AuthFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws ResponseException
*/
public function getQuota(): array {
$this->checkConnection();
return $this->connection->getQuota($this->username)->validatedData();
}
/**
* Retrieve the quota settings per user
* @param string $quota_root
*
* @return array
* @throws ConnectionFailedException
* @throws AuthFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws ResponseException
*/
public function getQuotaRoot(string $quota_root = 'INBOX'): array {
$this->checkConnection();
return $this->connection->getQuotaRoot($quota_root)->validatedData();
}
/**
* Delete all messages marked for deletion
*
* @return array
* @throws ConnectionFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws AuthFailedException
* @throws ResponseException
*/
public function expunge(): array {
$this->checkConnection();
return $this->connection->expunge()->validatedData();
}
/**
* Set the connection timeout
* @param integer $timeout
*
* @return ProtocolInterface
* @throws ConnectionFailedException
* @throws AuthFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws ResponseException
*/
public function setTimeout(int $timeout): ProtocolInterface {
$this->timeout = $timeout;
if ($this->isConnected()) {
$this->connection->setConnectionTimeout($timeout);
$this->reconnect();
}
return $this->connection;
}
/**
* Get the connection timeout
*
* @return int
* @throws ConnectionFailedException
* @throws AuthFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws ResponseException
*/
public function getTimeout(): int {
$this->checkConnection();
return $this->connection->getConnectionTimeout();
}
/**
* Get the default message mask
*
* @return string
*/
public function getDefaultMessageMask(): string {
return $this->default_message_mask;
}
/**
* Get the default events for a given section
* @param $section
*
* @return array
*/
public function getDefaultEvents($section): array {
if (isset($this->events[$section])) {
return is_array($this->events[$section]) ? $this->events[$section] : [];
}
return [];
}
/**
* Set the default message mask
* @param string $mask
*
* @return $this
* @throws MaskNotFoundException
*/
public function setDefaultMessageMask(string $mask): Client {
if(class_exists($mask)) {
$this->default_message_mask = $mask;
return $this;
}
throw new MaskNotFoundException("Unknown mask provided: ".$mask);
}
/**
* Get the default attachment mask
*
* @return string
*/
public function getDefaultAttachmentMask(): string {
return $this->default_attachment_mask;
}
/**
* Set the default attachment mask
* @param string $mask
*
* @return $this
* @throws MaskNotFoundException
*/
public function setDefaultAttachmentMask(string $mask): Client {
if(class_exists($mask)) {
$this->default_attachment_mask = $mask;
return $this;
}
throw new MaskNotFoundException("Unknown mask provided: ".$mask);
}
}

View File

@@ -0,0 +1,131 @@
<?php
/*
* File: ClientManager.php
* Category: -
* Author: M. Goldenbaum
* Created: 19.01.17 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP;
/**
* Class ClientManager
*
* @package Webklex\IMAP
*/
class ClientManager {
/**
* All library config
*
* @var Config $config
*/
public Config $config;
/**
* @var array $accounts
*/
protected array $accounts = [];
/**
* ClientManager constructor.
* @param array|string|Config $config
*/
public function __construct(array|string|Config $config = []) {
$this->setConfig($config);
}
/**
* Dynamically pass calls to the default account.
* @param string $method
* @param array $parameters
*
* @return mixed
* @throws Exceptions\MaskNotFoundException
*/
public function __call(string $method, array $parameters) {
$callable = [$this->account(), $method];
return call_user_func_array($callable, $parameters);
}
/**
* Safely create a new client instance which is not listed in accounts
* @param array $config
*
* @return Client
* @throws Exceptions\MaskNotFoundException
*/
public function make(array $config): Client {
$name = $this->config->getDefaultAccount();
$clientConfig = $this->config->all();
$clientConfig["accounts"] = [$name => $config];
return new Client(Config::make($clientConfig));
}
/**
* Resolve a account instance.
* @param string|null $name
*
* @return Client
* @throws Exceptions\MaskNotFoundException
*/
public function account(string $name = null): Client {
$name = $name ?: $this->config->getDefaultAccount();
// If the connection has not been resolved we will resolve it now as all
// the connections are resolved when they are actually needed, so we do
// not make any unnecessary connection to the various queue end-points.
if (!isset($this->accounts[$name])) {
$this->accounts[$name] = $this->resolve($name);
}
return $this->accounts[$name];
}
/**
* Resolve an account.
* @param string $name
*
* @return Client
* @throws Exceptions\MaskNotFoundException
*/
protected function resolve(string $name): Client {
$config = $this->config->getClientConfig($name);
return new Client($config);
}
/**
* Merge the vendor settings with the local config
*
* The default account identifier will be used as default for any missing account parameters.
* If however the default account is missing a parameter the package default account parameter will be used.
* This can be disabled by setting imap.default in your config file to 'false'
*
* @param array|string|Config $config
*
* @return $this
*/
public function setConfig(array|string|Config $config): ClientManager {
if (!$config instanceof Config) {
$config = Config::make($config);
}
$this->config = $config;
return $this;
}
/**
* Get the config instance
* @return Config
*/
public function getConfig(): Config {
return $this->config;
}
}

View File

@@ -0,0 +1,266 @@
<?php
/*
* File: Config.php
* Category: -
* Author: M.Goldenbaum
* Created: 10.04.24 15:42
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP;
/**
* Class Config
*
* @package Webklex\PHPIMAP
*/
class Config {
/**
* Configuration array
* @var array $config
*/
protected array $config = [];
/**
* Config constructor.
* @param array $config
*/
public function __construct(array $config = []) {
$this->config = $config;
}
/**
* Get a dotted config parameter
* @param string $key
* @param null $default
*
* @return mixed|null
*/
public function get(string $key, $default = null): mixed {
$parts = explode('.', $key);
$value = null;
foreach ($parts as $part) {
if ($value === null) {
if (isset($this->config[$part])) {
$value = $this->config[$part];
} else {
break;
}
} else {
if (isset($value[$part])) {
$value = $value[$part];
} else {
break;
}
}
}
return $value === null ? $default : $value;
}
/**
* Set a dotted config parameter
* @param string $key
* @param string|array|mixed$value
*
* @return void
*/
public function set(string $key, mixed $value): void {
$parts = explode('.', $key);
$config = &$this->config;
foreach ($parts as $part) {
if (!isset($config[$part])) {
$config[$part] = [];
}
$config = &$config[$part];
}
if(is_array($config) && is_array($value)){
$config = array_merge($config, $value);
}else{
$config = $value;
}
}
/**
* Get the mask for a given section
* @param string $section section name such as "message" or "attachment"
*
* @return string|null
*/
public function getMask(string $section): ?string {
$default_masks = $this->get('masks', []);
if (isset($default_masks[$section])) {
if (class_exists($default_masks[$section])) {
return $default_masks[$section];
}
}
return null;
}
/**
* Get the account configuration.
* @param string|null $name
*
* @return self
*/
public function getClientConfig(?string $name): self {
$config = $this->all();
$defaultName = $this->getDefaultAccount();
$defaultAccount = $this->get('accounts.'.$defaultName, []);
if ($name === null || $name === 'null' || $name === "") {
$account = $defaultAccount;
$name = $defaultName;
}else{
$account = $this->get('accounts.'.$name, $defaultAccount);
}
$config["default"] = $name;
$config["accounts"] = [
$name => $account
];
return new self($config);
}
/**
* Get the name of the default account.
*
* @return string
*/
public function getDefaultAccount(): string {
return $this->get('default', 'default');
}
/**
* Set the name of the default account.
* @param string $name
*
* @return void
*/
public function setDefaultAccount(string $name): void {
$this->set('default', $name);
}
/**
* Create a new instance of the Config class
* @param array|string $config
* @return Config
*/
public static function make(array|string $config = []): Config {
if (is_array($config) === false) {
$config = require $config;
}
$config_key = 'imap';
$path = __DIR__ . '/config/' . $config_key . '.php';
$vendor_config = require $path;
$config = self::array_merge_recursive_distinct($vendor_config, $config);
if (isset($config['default'])) {
if (isset($config['accounts']) && $config['default']) {
$default_config = $vendor_config['accounts']['default'];
if (isset($config['accounts'][$config['default']])) {
$default_config = array_merge($default_config, $config['accounts'][$config['default']]);
}
if (is_array($config['accounts'])) {
foreach ($config['accounts'] as $account_key => $account) {
$config['accounts'][$account_key] = array_merge($default_config, $account);
}
}
}
}
return new self($config);
}
/**
* Marge arrays recursively and distinct
*
* Merges any number of arrays / parameters recursively, replacing
* entries with string keys with values from latter arrays.
* If the entry or the next value to be assigned is an array, then it
* automatically treats both arguments as an array.
* Numeric entries are appended, not replaced, but only if they are
* unique
*
* @return array
*
* @link http://www.php.net/manual/en/function.array-merge-recursive.php#96201
* @author Mark Roduner <mark.roduner@gmail.com>
*/
private static function array_merge_recursive_distinct(): array {
$arrays = func_get_args();
$base = array_shift($arrays);
// From https://stackoverflow.com/a/173479
$isAssoc = function(array $arr) {
if (array() === $arr) return false;
return array_keys($arr) !== range(0, count($arr) - 1);
};
if (!is_array($base)) $base = empty($base) ? array() : array($base);
foreach ($arrays as $append) {
if (!is_array($append)) $append = array($append);
foreach ($append as $key => $value) {
if (!array_key_exists($key, $base) and !is_numeric($key)) {
$base[$key] = $value;
continue;
}
if ((is_array($value) && $isAssoc($value)) || (is_array($base[$key]) && $isAssoc($base[$key]))) {
// If the arrays are not associates we don't want to array_merge_recursive_distinct
// else merging $baseConfig['dispositions'] = ['attachment', 'inline'] with $customConfig['dispositions'] = ['attachment']
// results in $resultConfig['dispositions'] = ['attachment', 'inline']
$base[$key] = self::array_merge_recursive_distinct($base[$key], $value);
} else if (is_numeric($key)) {
if (!in_array($value, $base)) $base[] = $value;
} else {
$base[$key] = $value;
}
}
}
return $base;
}
/**
* Get all configuration values
* @return array
*/
public function all(): array {
return $this->config;
}
/**
* Check if a configuration value exists
* @param string $key
* @return bool
*/
public function has(string $key): bool {
return $this->get($key) !== null;
}
/**
* Remove all configuration values
* @return $this
*/
public function clear(): static {
$this->config = [];
return $this;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,825 @@
<?php
/*
* File: LegacyProtocol.php
* Category: Protocol
* Author: M.Goldenbaum
* Created: 16.09.20 18:27
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Connection\Protocols;
use Webklex\PHPIMAP\ClientManager;
use Webklex\PHPIMAP\Config;
use Webklex\PHPIMAP\Exceptions\AuthFailedException;
use Webklex\PHPIMAP\Exceptions\ImapBadRequestException;
use Webklex\PHPIMAP\Exceptions\MethodNotSupportedException;
use Webklex\PHPIMAP\Exceptions\RuntimeException;
use Webklex\PHPIMAP\IMAP;
/**
* Class LegacyProtocol
*
* @package Webklex\PHPIMAP\Connection\Protocols
*/
class LegacyProtocol extends Protocol {
protected string $protocol = "imap";
protected string $host = "localhost";
protected int $port = 993;
/**
* Imap constructor.
* @param Config $config
* @param bool $cert_validation set to false to skip SSL certificate validation
* @param mixed $encryption Connection encryption method
*/
public function __construct(Config $config, bool $cert_validation = true, mixed $encryption = false) {
$this->config = $config;
$this->setCertValidation($cert_validation);
$this->encryption = $encryption;
}
/**
* Public destructor
*/
public function __destruct() {
$this->logout();
}
/**
* Save the information for a nw connection
* @param string $host
* @param int|null $port
*/
public function connect(string $host, int $port = null): void {
if ($this->encryption) {
$encryption = strtolower($this->encryption);
if ($encryption == "ssl") {
$port = $port === null ? 993 : $port;
}
}
$port = $port === null ? 143 : $port;
$this->host = $host;
$this->port = $port;
}
/**
* Login to a new session.
* @param string $user username
* @param string $password password
*
* @return Response
*/
public function login(string $user, string $password): Response {
return $this->response()->wrap(function($response) use ($user, $password) {
/** @var Response $response */
try {
$this->stream = \imap_open(
$this->getAddress(),
$user,
$password,
0,
$attempts = 3,
$this->config->get('options.open')
);
$response->addCommand("imap_open");
} catch (\ErrorException $e) {
$errors = \imap_errors();
$message = $e->getMessage() . '. ' . implode("; ", (is_array($errors) ? $errors : array()));
throw new AuthFailedException($message);
}
if (!$this->stream) {
$errors = \imap_errors();
$message = implode("; ", (is_array($errors) ? $errors : array()));
throw new AuthFailedException($message);
}
$errors = \imap_errors();
$response->addCommand("imap_errors");
if (is_array($errors)) {
$status = $this->examineFolder();
$response->stack($status);
if ($status->data()['exists'] !== 0) {
$message = implode("; ", $errors);
throw new RuntimeException($message);
}
}
if ($this->stream !== false) {
return ["TAG" . $response->Noun() . " OK [] Logged in\r\n"];
}
$response->addError("failed to login");
return [];
});
}
/**
* Authenticate your current session.
* @param string $user username
* @param string $token access token
*
* @return Response
*/
public function authenticate(string $user, string $token): Response {
return $this->login($user, $token);
}
/**
* Get full address of mailbox.
*
* @return string
*/
protected function getAddress(): string {
$address = "{" . $this->host . ":" . $this->port . "/" . $this->protocol;
if (!$this->cert_validation) {
$address .= '/novalidate-cert';
}
if (in_array($this->encryption, ['tls', 'notls', 'ssl'])) {
$address .= '/' . $this->encryption;
} elseif ($this->encryption === "starttls") {
$address .= '/tls';
}
$address .= '}';
return $address;
}
/**
* Logout of the current session
*
* @return Response
*/
public function logout(): Response {
return $this->response()->wrap(function($response) {
/** @var Response $response */
if ($this->stream) {
$this->uid_cache = [];
$response->addCommand("imap_close");
if (\imap_close($this->stream, IMAP::CL_EXPUNGE)) {
$this->stream = false;
return [
0 => "BYE Logging out\r\n",
1 => "TAG" . $response->Noun() . " OK Logout completed (0.001 + 0.000 secs).\r\n",
];
}
$this->stream = false;
}
return [];
});
}
/**
* Get an array of available capabilities
*
* @throws MethodNotSupportedException
*/
public function getCapabilities(): Response {
throw new MethodNotSupportedException();
}
/**
* Change the current folder
* @param string $folder change to this folder
*
* @return Response see examineOrselect()
* @throws RuntimeException
*/
public function selectFolder(string $folder = 'INBOX'): Response {
$flags = IMAP::OP_READONLY;
if (in_array($this->protocol, ["pop3", "nntp"])) {
$flags = IMAP::NIL;
}
if ($this->stream === false) {
throw new RuntimeException("failed to reopen stream.");
}
return $this->response("imap_reopen")->wrap(function($response) use ($folder, $flags) {
/** @var Response $response */
\imap_reopen($this->stream, $this->getAddress() . $folder, $flags, 3);
$this->uid_cache = [];
$status = $this->examineFolder($folder);
$response->stack($status);
return $status->data();
});
}
/**
* Examine a given folder
* @param string $folder examine this folder
*
* @return Response
* @throws RuntimeException
*/
public function examineFolder(string $folder = 'INBOX'): Response {
if (str_starts_with($folder, ".")) {
throw new RuntimeException("Segmentation fault prevented. Folders starts with an illegal char '.'.");
}
return $this->response("imap_status")->wrap(function($response) use ($folder) {
/** @var Response $response */
$status = \imap_status($this->stream, $this->getAddress() . $folder, IMAP::SA_ALL);
return $status ? [
"flags" => [],
"exists" => $status->messages,
"recent" => $status->recent,
"unseen" => $status->unseen,
"uidnext" => $status->uidnext,
] : [];
});
}
/**
* Get the status of a given folder
*
* @return Response list of STATUS items
* @throws MethodNotSupportedException
*/
public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): Response {
throw new MethodNotSupportedException();
}
/**
* Fetch message content
* @param int|array $uids
* @param string $rfc
* @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers.
*
* @return Response
*/
public function content(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response {
return $this->response()->wrap(function($response) use ($uids, $uid) {
/** @var Response $response */
$result = [];
$uids = is_array($uids) ? $uids : [$uids];
foreach ($uids as $id) {
$response->addCommand("imap_fetchbody");
$result[$id] = \imap_fetchbody($this->stream, $id, "", $uid === IMAP::ST_UID ? IMAP::ST_UID : IMAP::NIL);
}
return $result;
});
}
/**
* Fetch message headers
* @param int|array $uids
* @param string $rfc
* @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers.
*
* @return Response
*/
public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response {
return $this->response()->wrap(function($response) use ($uids, $uid) {
/** @var Response $response */
$result = [];
$uids = is_array($uids) ? $uids : [$uids];
foreach ($uids as $id) {
$response->addCommand("imap_fetchheader");
$result[$id] = \imap_fetchheader($this->stream, $id, $uid ? IMAP::ST_UID : IMAP::NIL);
}
return $result;
});
}
/**
* Fetch message flags
* @param int|array $uids
* @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers.
*
* @return Response
*/
public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response {
return $this->response()->wrap(function($response) use ($uids, $uid) {
/** @var Response $response */
$result = [];
$uids = is_array($uids) ? $uids : [$uids];
foreach ($uids as $id) {
$response->addCommand("imap_fetch_overview");
$raw_flags = \imap_fetch_overview($this->stream, $id, $uid ? IMAP::ST_UID : IMAP::NIL);
$flags = [];
if (is_array($raw_flags) && isset($raw_flags[0])) {
$raw_flags = (array)$raw_flags[0];
foreach ($raw_flags as $flag => $value) {
if ($value === 1 && in_array($flag, ["size", "uid", "msgno", "update"]) === false) {
$flags[] = "\\" . ucfirst($flag);
}
}
}
$result[$id] = $flags;
}
return $result;
});
}
/**
* Fetch message sizes
* @param int|array $uids
* @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers.
*
* @return Response
*/
public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response {
return $this->response()->wrap(function($response) use ($uids, $uid) {
/** @var Response $response */
$result = [];
$uids = is_array($uids) ? $uids : [$uids];
$uid_text = implode("','", $uids);
$response->addCommand("imap_fetch_overview");
if ($uid == IMAP::ST_UID) {
$raw_overview = \imap_fetch_overview($this->stream, $uid_text, IMAP::FT_UID);
} else {
$raw_overview = \imap_fetch_overview($this->stream, $uid_text);
}
if ($raw_overview !== false) {
foreach ($raw_overview as $overview_element) {
$overview_element = (array)$overview_element;
$result[$overview_element[$uid == IMAP::ST_UID ? 'uid' : 'msgno']] = $overview_element['size'];
}
}
return $result;
});
}
/**
* Get uid for a given id
* @param int|null $id message number
*
* @return Response message number for given message or all messages as array
*/
public function getUid(int $id = null): Response {
return $this->response()->wrap(function($response) use ($id) {
/** @var Response $response */
if ($id === null) {
if ($this->enable_uid_cache && $this->uid_cache) {
return $this->uid_cache;
}
$overview = $this->overview("1:*");
$response->stack($overview);
$uids = [];
foreach ($overview->data() as $set) {
$uids[$set->msgno] = $set->uid;
}
$this->setUidCache($uids);
return $uids;
}
$response->addCommand("imap_uid");
$uid = \imap_uid($this->stream, $id);
if ($uid) {
return $uid;
}
return [];
});
}
/**
* Get the message number of a given uid
* @param string $id uid
*
* @return Response message number
*/
public function getMessageNumber(string $id): Response {
return $this->response("imap_msgno")->wrap(function($response) use ($id) {
/** @var Response $response */
return \imap_msgno($this->stream, $id);
});
}
/**
* Get a message overview
* @param string $sequence uid sequence
* @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers.
*
* @return Response
*/
public function overview(string $sequence, int|string $uid = IMAP::ST_UID): Response {
return $this->response("imap_fetch_overview")->wrap(function($response) use ($sequence, $uid) {
/** @var Response $response */
return \imap_fetch_overview($this->stream, $sequence, $uid ? IMAP::ST_UID : IMAP::NIL) ?: [];
});
}
/**
* Get a list of available folders
* @param string $reference mailbox reference for list
* @param string $folder mailbox name match with wildcards
*
* @return Response folders that matched $folder as array(name => array('delimiter' => .., 'flags' => ..))
*/
public function folders(string $reference = '', string $folder = '*'): Response {
return $this->response("imap_getmailboxes")->wrap(function($response) use ($reference, $folder) {
/** @var Response $response */
$result = [];
$items = \imap_getmailboxes($this->stream, $this->getAddress(), $reference . $folder);
if (is_array($items)) {
foreach ($items as $item) {
$name = $this->decodeFolderName($item->name);
$result[$name] = ['delimiter' => $item->delimiter, 'flags' => []];
}
} else {
throw new RuntimeException(\imap_last_error());
}
return $result;
});
}
/**
* Manage flags
* @param array|string $flags flags to set, add or remove - see $mode
* @param int $from message for items or start message if $to !== null
* @param int|null $to if null only one message ($from) is fetched, else it's the
* last message, INF means last message available
* @param string|null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given
* @param bool $silent if false the return values are the new flags for the wanted messages
* @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers.
* @param string|null $item unused attribute
*
* @return Response new flags if $silent is false, else true or false depending on success
*/
public function store(array|string $flags, int $from, int $to = null, string $mode = null, bool $silent = true, int|string $uid = IMAP::ST_UID, string $item = null): Response {
$flag = trim(is_array($flags) ? implode(" ", $flags) : $flags);
return $this->response()->wrap(function($response) use ($mode, $from, $flag, $uid, $silent) {
/** @var Response $response */
if ($mode == "+") {
$response->addCommand("imap_setflag_full");
$status = \imap_setflag_full($this->stream, $from, $flag, $uid ? IMAP::ST_UID : IMAP::NIL);
} else {
$response->addCommand("imap_clearflag_full");
$status = \imap_clearflag_full($this->stream, $from, $flag, $uid ? IMAP::ST_UID : IMAP::NIL);
}
if ($silent === true) {
if ($status) {
return [
"TAG" . $response->Noun() . " OK Store completed (0.001 + 0.000 secs).\r\n"
];
}
return [];
}
return $this->flags($from);
});
}
/**
* Append a new message to given folder
* @param string $folder name of target folder
* @param string $message full message content
* @param array|null $flags flags for new message
* @param mixed $date date for new message
*
* @return Response
*/
public function appendMessage(string $folder, string $message, array $flags = null, mixed $date = null): Response {
return $this->response("imap_append")->wrap(function($response) use ($folder, $message, $flags, $date) {
/** @var Response $response */
if ($date != null) {
if ($date instanceof \Carbon\Carbon) {
$date = $date->format('d-M-Y H:i:s O');
}
if (\imap_append($this->stream, $this->getAddress() . $folder, $message, $flags, $date)) {
return [
"OK Append completed (0.001 + 0.000 secs).\r\n"
];
}
} else if (\imap_append($this->stream, $this->getAddress() . $folder, $message, $flags)) {
return [
"OK Append completed (0.001 + 0.000 secs).\r\n"
];
}
return [];
});
}
/**
* Copy message set from current folder to other folder
* @param string $folder destination folder
* @param $from
* @param int|null $to if null only one message ($from) is fetched, else it's the
* last message, INF means last message available
* @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers.
*
* @return Response
*/
public function copyMessage(string $folder, $from, int $to = null, int|string $uid = IMAP::ST_UID): Response {
return $this->response("imap_mail_copy")->wrap(function($response) use ($from, $folder, $uid) {
/** @var Response $response */
if (\imap_mail_copy($this->stream, $from, $this->getAddress() . $folder, $uid ? IMAP::ST_UID : IMAP::NIL)) {
return [
"TAG" . $response->Noun() . " OK Copy completed (0.001 + 0.000 secs).\r\n"
];
}
throw new ImapBadRequestException("Invalid ID $from");
});
}
/**
* Copy multiple messages to the target folder
* @param array $messages List of message identifiers
* @param string $folder Destination folder
* @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers.
*
* @return Response Tokens if operation successful, false if an error occurred
*/
public function copyManyMessages(array $messages, string $folder, int|string $uid = IMAP::ST_UID): Response {
return $this->response()->wrap(function($response) use ($messages, $folder, $uid) {
/** @var Response $response */
foreach ($messages as $msg) {
$copy_response = $this->copyMessage($folder, $msg, null, $uid);
$response->stack($copy_response);
if (empty($copy_response->data())) {
return [
"TAG" . $response->Noun() . " BAD Copy failed (0.001 + 0.000 secs).\r\n",
"Invalid ID $msg\r\n"
];
}
}
return [
"TAG" . $response->Noun() . " OK Copy completed (0.001 + 0.000 secs).\r\n"
];
});
}
/**
* Move a message set from current folder to another folder
* @param string $folder destination folder
* @param $from
* @param int|null $to if null only one message ($from) is fetched, else it's the
* last message, INF means last message available
* @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers.
*
* @return Response success
*/
public function moveMessage(string $folder, $from, int $to = null, int|string $uid = IMAP::ST_UID): Response {
return $this->response("imap_mail_move")->wrap(function($response) use ($from, $folder, $uid) {
if (\imap_mail_move($this->stream, $from, $this->getAddress() . $folder, $uid ? IMAP::ST_UID : IMAP::NIL)) {
return [
"TAG" . $response->Noun() . " OK Move completed (0.001 + 0.000 secs).\r\n"
];
}
throw new ImapBadRequestException("Invalid ID $from");
});
}
/**
* Move multiple messages to the target folder
* @param array $messages List of message identifiers
* @param string $folder Destination folder
* @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers.
*
* @return Response Tokens if operation successful, false if an error occurred
* @throws ImapBadRequestException
*/
public function moveManyMessages(array $messages, string $folder, int|string $uid = IMAP::ST_UID): Response {
return $this->response()->wrap(function($response) use ($messages, $folder, $uid) {
foreach ($messages as $msg) {
$move_response = $this->moveMessage($folder, $msg, null, $uid);
$response = $response->include($response);
if (empty($move_response->data())) {
return [
"TAG" . $response->Noun() . " BAD Move failed (0.001 + 0.000 secs).\r\n",
"Invalid ID $msg\r\n"
];
}
}
return [
"TAG" . $response->Noun() . " OK Move completed (0.001 + 0.000 secs).\r\n"
];
});
}
/**
* Exchange identification information
* Ref.: https://datatracker.ietf.org/doc/html/rfc2971
*
* @param null $ids
* @return Response
*
* @throws MethodNotSupportedException
*/
public function ID($ids = null): Response {
throw new MethodNotSupportedException();
}
/**
* Create a new folder (and parent folders if needed)
* @param string $folder folder name
*
* @return Response
*/
public function createFolder(string $folder): Response {
return $this->response("imap_createmailbox")->wrap(function($response) use ($folder) {
return \imap_createmailbox($this->stream, $this->getAddress() . $folder) ? [
0 => "TAG" . $response->Noun() . " OK Create completed (0.004 + 0.000 + 0.003 secs).\r\n",
] : [];
});
}
/**
* Rename an existing folder
* @param string $old old name
* @param string $new new name
*
* @return Response
*/
public function renameFolder(string $old, string $new): Response {
return $this->response("imap_renamemailbox")->wrap(function($response) use ($old, $new) {
return \imap_renamemailbox($this->stream, $this->getAddress() . $old, $this->getAddress() . $new) ? [
0 => "TAG" . $response->Noun() . " OK Move completed (0.004 + 0.000 + 0.003 secs).\r\n",
] : [];
});
}
/**
* Delete a folder
* @param string $folder folder name
*
* @return Response
*/
public function deleteFolder(string $folder): Response {
return $this->response("imap_deletemailbox")->wrap(function($response) use ($folder) {
return \imap_deletemailbox($this->stream, $this->getAddress() . $folder) ? [
0 => "OK Delete completed (0.004 + 0.000 + 0.003 secs).\r\n",
] : [];
});
}
/**
* Subscribe to a folder
* @param string $folder folder name
*
* @throws MethodNotSupportedException
*/
public function subscribeFolder(string $folder): Response {
throw new MethodNotSupportedException();
}
/**
* Unsubscribe from a folder
* @param string $folder folder name
*
* @throws MethodNotSupportedException
*/
public function unsubscribeFolder(string $folder): Response {
throw new MethodNotSupportedException();
}
/**
* Apply session saved changes to the server
*
* @return Response
*/
public function expunge(): Response {
return $this->response("imap_expunge")->wrap(function($response) {
return \imap_expunge($this->stream) ? [
0 => "TAG" . $response->Noun() . " OK Expunge completed (0.001 + 0.000 secs).\r\n",
] : [];
});
}
/**
* Send noop command
*
* @throws MethodNotSupportedException
*/
public function noop(): Response {
throw new MethodNotSupportedException();
}
/**
* Send idle command
*
* @throws MethodNotSupportedException
*/
public function idle() {
throw new MethodNotSupportedException();
}
/**
* Send done command
*
* @throws MethodNotSupportedException
*/
public function done() {
throw new MethodNotSupportedException();
}
/**
* Search for matching messages
* @param array $params
* @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers.
*
* @return Response message ids
*/
public function search(array $params, int|string $uid = IMAP::ST_UID): Response {
return $this->response("imap_search")->wrap(function($response) use ($params, $uid) {
$response->setCanBeEmpty(true);
$result = \imap_search($this->stream, $params[0], $uid ? IMAP::ST_UID : IMAP::NIL);
return $result ?: [];
});
}
/**
* Enable the debug mode
*/
public function enableDebug() {
$this->debug = true;
}
/**
* Disable the debug mode
*/
public function disableDebug() {
$this->debug = false;
}
/**
* Decode name.
* It converts UTF7-IMAP encoding to UTF-8.
*
* @param $name
*
* @return array|false|string|string[]|null
*/
protected function decodeFolderName($name): array|bool|string|null {
preg_match('#\{(.*)}(.*)#', $name, $preg);
return mb_convert_encoding($preg[2], "UTF-8", "UTF7-IMAP");
}
/**
* @return string
*/
public function getProtocol(): string {
return $this->protocol;
}
/**
* Retrieve the quota level settings, and usage statics per mailbox
* @param $username
*
* @return Response
*/
public function getQuota($username): Response {
return $this->response("imap_get_quota")->wrap(function($response) use ($username) {
$result = \imap_get_quota($this->stream, 'user.' . $username);
return $result ?: [];
});
}
/**
* Retrieve the quota settings per user
* @param string $quota_root
*
* @return Response
*/
public function getQuotaRoot(string $quota_root = 'INBOX'): Response {
return $this->response("imap_get_quotaroot")->wrap(function($response) use ($quota_root) {
$result = \imap_get_quotaroot($this->stream, $this->getAddress() . $quota_root);
return $result ?: [];
});
}
/**
* @param string $protocol
* @return LegacyProtocol
*/
public function setProtocol(string $protocol): LegacyProtocol {
if (($pos = strpos($protocol, "legacy")) > 0) {
$protocol = substr($protocol, 0, ($pos + 2) * -1);
}
$this->protocol = $protocol;
return $this;
}
/**
* Create a new Response instance
* @param string|null $command
*
* @return Response
*/
protected function response(?string $command = ""): Response {
return Response::make(0, $command == "" ? [] : [$command], [], $this->debug);
}
}

View File

@@ -0,0 +1,381 @@
<?php
/*
* File: ImapProtocol.php
* Category: Protocol
* Author: M.Goldenbaum
* Created: 16.09.20 18:27
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Connection\Protocols;
use Webklex\PHPIMAP\Config;
use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
use Webklex\PHPIMAP\IMAP;
/**
* Class Protocol
*
* @package Webklex\PHPIMAP\Connection\Protocols
*/
abstract class Protocol implements ProtocolInterface {
/**
* Default connection timeout in seconds
*/
protected int $connection_timeout = 30;
/**
* @var boolean
*/
protected bool $debug = false;
/**
* @var boolean
*/
protected bool $enable_uid_cache = true;
/**
* @var resource|mixed|boolean|null $stream
*/
public $stream = false;
/**
* @var Config $config
*/
protected Config $config;
/**
* Connection encryption method
* @var string $encryption
*/
protected string $encryption = "";
/**
* Set to false to ignore SSL certificate validation
* @var bool
*/
protected bool $cert_validation = true;
/**
* Proxy settings
* @var array
*/
protected array $proxy = [
'socket' => null,
'request_fulluri' => false,
'username' => null,
'password' => null,
];
/**
* Cache for uid of active folder.
*
* @var array
*/
protected array $uid_cache = [];
/**
* Get an available cryptographic method
*
* @return int
*/
public function getCryptoMethod(): int {
// Allow the best TLS version(s) we can
$cryptoMethod = STREAM_CRYPTO_METHOD_TLS_CLIENT;
// PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT
// so add them back in manually if we can
if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
$cryptoMethod = STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
}elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT')) {
$cryptoMethod = STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
}
return $cryptoMethod;
}
/**
* Enable SSL certificate validation
*
* @return Protocol
*/
public function enableCertValidation(): Protocol {
$this->cert_validation = true;
return $this;
}
/**
* Disable SSL certificate validation
* @return Protocol
*/
public function disableCertValidation(): Protocol {
$this->cert_validation = false;
return $this;
}
/**
* Set SSL certificate validation
* @var int $cert_validation
*
* @return Protocol
*/
public function setCertValidation(int $cert_validation): Protocol {
$this->cert_validation = $cert_validation;
return $this;
}
/**
* Should we validate SSL certificate?
*
* @return bool
*/
public function getCertValidation(): bool {
return $this->cert_validation;
}
/**
* Set connection proxy settings
* @var array $options
*
* @return Protocol
*/
public function setProxy(array $options): Protocol {
foreach ($this->proxy as $key => $val) {
if (isset($options[$key])) {
$this->proxy[$key] = $options[$key];
}
}
return $this;
}
/**
* Get the current proxy settings
*
* @return array
*/
public function getProxy(): array {
return $this->proxy;
}
/**
* Prepare socket options
* @return array
*@var string $transport
*
*/
private function defaultSocketOptions(string $transport): array {
$options = [];
if ($this->encryption) {
$options["ssl"] = [
'verify_peer_name' => $this->getCertValidation(),
'verify_peer' => $this->getCertValidation(),
];
}
if ($this->proxy["socket"] != null) {
$options[$transport]["proxy"] = $this->proxy["socket"];
$options[$transport]["request_fulluri"] = $this->proxy["request_fulluri"];
if ($this->proxy["username"] != null) {
$auth = base64_encode($this->proxy["username"].':'.$this->proxy["password"]);
$options[$transport]["header"] = [
"Proxy-Authorization: Basic $auth"
];
}
}
return $options;
}
/**
* Create a new resource stream
* @param $transport
* @param string $host hostname or IP address of IMAP server
* @param int $port of IMAP server, default is 143 (993 for ssl)
* @param int $timeout timeout in seconds for initiating session
*
* @return resource The socket created.
* @throws ConnectionFailedException
*/
public function createStream($transport, string $host, int $port, int $timeout) {
$socket = "$transport://$host:$port";
$stream = stream_socket_client($socket, $errno, $errstr, $timeout,
STREAM_CLIENT_CONNECT,
stream_context_create($this->defaultSocketOptions($transport))
);
if (!$stream) {
throw new ConnectionFailedException($errstr, $errno);
}
if (false === stream_set_timeout($stream, $timeout)) {
throw new ConnectionFailedException('Failed to set stream timeout');
}
return $stream;
}
/**
* Get the current connection timeout
*
* @return int
*/
public function getConnectionTimeout(): int {
return $this->connection_timeout;
}
/**
* Set the connection timeout
* @param int $connection_timeout
*
* @return Protocol
*/
public function setConnectionTimeout(int $connection_timeout): Protocol {
$this->connection_timeout = $connection_timeout;
return $this;
}
/**
* Get the UID key string
* @param int|string $uid
*
* @return string
*/
public function getUIDKey(int|string $uid): string {
if ($uid == IMAP::ST_UID || $uid == IMAP::FT_UID) {
return "UID";
}
if (strlen($uid) > 0 && !is_numeric($uid)) {
return (string)$uid;
}
return "";
}
/**
* Build a UID / MSGN command
* @param string $command
* @param int|string $uid
*
* @return string
*/
public function buildUIDCommand(string $command, int|string $uid): string {
return trim($this->getUIDKey($uid)." ".$command);
}
/**
* Set the uid cache of current active folder
*
* @param array|null $uids
*/
public function setUidCache(?array $uids): void {
if (is_null($uids)) {
$this->uid_cache = [];
return;
}
$messageNumber = 1;
$uid_cache = [];
foreach ($uids as $uid) {
$uid_cache[$messageNumber++] = (int)$uid;
}
$this->uid_cache = $uid_cache;
}
/**
* Enable the uid cache
*
* @return void
*/
public function enableUidCache(): void {
$this->enable_uid_cache = true;
}
/**
* Disable the uid cache
*
* @return void
*/
public function disableUidCache(): void {
$this->enable_uid_cache = false;
}
/**
* Set the encryption method
* @param string $encryption
*
* @return void
*/
public function setEncryption(string $encryption): void {
$this->encryption = $encryption;
}
/**
* Get the encryption method
* @return string
*/
public function getEncryption(): string {
return $this->encryption;
}
/**
* Check if the current session is connected
*
* @return bool
*/
public function connected(): bool {
return (bool)$this->stream;
}
/**
* Retrieves header/metadata from the resource stream
*
* @return array
*/
public function meta(): array {
if (!$this->stream) {
return [
"crypto" => [
"protocol" => "",
"cipher_name" => "",
"cipher_bits" => 0,
"cipher_version" => "",
],
"timed_out" => true,
"blocked" => true,
"eof" => true,
"stream_type" => "tcp_socket/unknown",
"mode" => "c",
"unread_bytes" => 0,
"seekable" => false,
];
}
return stream_get_meta_data($this->stream);
}
/**
* Get the resource stream
*
* @return mixed
*/
public function getStream(): mixed {
return $this->stream;
}
/**
* Set the Config instance
*
* @return Config
*/
public function getConfig(): Config {
return $this->config;
}
}

View File

@@ -0,0 +1,458 @@
<?php
/*
* File: ImapProtocol.php
* Category: Protocol
* Author: M.Goldenbaum
* Created: 16.09.20 18:27
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Connection\Protocols;
use ErrorException;
use Webklex\PHPIMAP\Exceptions\AuthFailedException;
use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
use Webklex\PHPIMAP\Exceptions\ImapBadRequestException;
use Webklex\PHPIMAP\Exceptions\ImapServerErrorException;
use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException;
use Webklex\PHPIMAP\Exceptions\MessageNotFoundException;
use Webklex\PHPIMAP\Exceptions\RuntimeException;
use Webklex\PHPIMAP\IMAP;
/**
* Interface ProtocolInterface
*
* @package Webklex\PHPIMAP\Connection\Protocols
*/
interface ProtocolInterface {
/**
* Public destructor
*
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
*/
public function __destruct();
/**
* Open a new connection / session
* @param string $host hostname or IP address of IMAP server
* @param int|null $port of service server
*
* @throws ErrorException
* @throws ConnectionFailedException
* @throws RuntimeException
*/
public function connect(string $host, ?int $port = null);
/**
* Login to a new session.
*
* @param string $user username
* @param string $password password
*
* @return Response
*
* @throws AuthFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
*/
public function login(string $user, string $password): Response;
/**
* Authenticate your current session.
* @param string $user username
* @param string $token access token
*
* @return Response
* @throws AuthFailedException
*/
public function authenticate(string $user, string $token): Response;
/**
* Logout of the current server session
*
* @return Response
*
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
*/
public function logout(): Response;
/**
* Check if the current session is connected
*
* @return bool
*/
public function connected(): bool;
/**
* Get an array of available capabilities
*
* @return Response containing a list of capabilities
* @throws RuntimeException
*/
public function getCapabilities(): Response;
/**
* Change the current folder
* @param string $folder change to this folder
*
* @return Response see examineOrSelect()
* @throws RuntimeException
*/
public function selectFolder(string $folder = 'INBOX'): Response;
/**
* Examine a given folder
* @param string $folder
*
* @return Response
* @throws RuntimeException
*/
public function examineFolder(string $folder = 'INBOX'): Response;
/**
* Get the status of a given folder
*
* @return Response list of STATUS items
*
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
*/
public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): Response;
/**
* Fetch message headers
* @param int|array $uids
* @param string $rfc
* @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use
* message numbers instead.
*
* @return Response
* @throws RuntimeException
*/
public function content(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response;
/**
* Fetch message headers
* @param int|array $uids
* @param string $rfc
* @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use
* message numbers instead.
*
* @return Response
* @throws RuntimeException
*/
public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response;
/**
* Fetch message flags
* @param int|array $uids
* @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use
* message numbers instead.
*
* @return Response
* @throws RuntimeException
*/
public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response;
/**
* Fetch message sizes
* @param int|array $uids
* @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use
* message numbers instead.
*
* @return Response
* @throws RuntimeException
*/
public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response;
/**
* Get uid for a given id
* @param int|null $id message number
*
* @return Response containing a message number for given message or all messages as array
* @throws MessageNotFoundException
*/
public function getUid(?int $id = null): Response;
/**
* Get a message number for a uid
* @param string $id uid
*
* @return Response containing the message number
* @throws MessageNotFoundException
*/
public function getMessageNumber(string $id): Response;
/**
* Get a list of available folders
* @param string $reference mailbox reference for list
* @param string $folder mailbox / folder name match with wildcards
*
* @return Response containing mailboxes that matched $folder as array(globalName => array('delim' => .., 'flags' => ..))
* @throws RuntimeException
*/
public function folders(string $reference = '', string $folder = '*'): Response;
/**
* Set message flags
* @param array|string $flags flags to set, add or remove
* @param int $from message for items or start message if $to !== null
* @param int|null $to if null only one message ($from) is fetched, else it's the
* last message, INF means last message available
* @param string|null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given
* @param bool $silent if false the return values are the new flags for the wanted messages
* @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use
* message numbers instead.
* @param string|null $item command used to store a flag
*
* @return Response containing the new flags if $silent is false, else true or false depending on success
* @throws RuntimeException
*/
public function store(array|string $flags, int $from, ?int $to = null, ?string $mode = null, bool $silent = true, int|string $uid = IMAP::ST_UID, ?string $item = null): Response;
/**
* Append a new message to given folder
* @param string $folder name of target folder
* @param string $message full message content
* @param array|null $flags flags for new message
* @param string|null $date date for new message
*
* @return Response
* @throws RuntimeException
*/
public function appendMessage(string $folder, string $message, ?array $flags = null, ?string $date = null): Response;
/**
* Copy message set from current folder to other folder
*
* @param string $folder destination folder
* @param $from
* @param int|null $to if null only one message ($from) is fetched, else it's the
* last message, INF means last message available
* @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use
* message numbers instead.
*
* @return Response
* @throws RuntimeException
*/
public function copyMessage(string $folder, $from, ?int $to = null, int|string $uid = IMAP::ST_UID): Response;
/**
* Copy multiple messages to the target folder
* @param array<string> $messages List of message identifiers
* @param string $folder Destination folder
* @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use
* message numbers instead.
*
* @return Response Tokens if operation successful, false if an error occurred
* @throws RuntimeException
*/
public function copyManyMessages(array $messages, string $folder, int|string $uid = IMAP::ST_UID): Response;
/**
* Move a message set from current folder to another folder
* @param string $folder destination folder
* @param $from
* @param int|null $to if null only one message ($from) is fetched, else it's the
* last message, INF means last message available
* @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use
* message numbers instead.
*
* @return Response
*/
public function moveMessage(string $folder, $from, ?int $to = null, int|string $uid = IMAP::ST_UID): Response;
/**
* Move multiple messages to the target folder
*
* @param array<string> $messages List of message identifiers
* @param string $folder Destination folder
* @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use
* message numbers instead.
*
* @return Response Tokens if operation successful, false if an error occurred
* @throws RuntimeException
*/
public function moveManyMessages(array $messages, string $folder, int|string $uid = IMAP::ST_UID): Response;
/**
* Exchange identification information
* Ref.: https://datatracker.ietf.org/doc/html/rfc2971
*
* @param null $ids
* @return Response
*
* @throws RuntimeException
*/
public function ID($ids = null): Response;
/**
* Create a new folder
*
* @param string $folder folder name
* @return Response
* @throws RuntimeException
*/
public function createFolder(string $folder): Response;
/**
* Rename an existing folder
*
* @param string $old old name
* @param string $new new name
* @return Response
* @throws RuntimeException
*/
public function renameFolder(string $old, string $new): Response;
/**
* Delete a folder
*
* @param string $folder folder name
* @return Response
*
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
*/
public function deleteFolder(string $folder): Response;
/**
* Subscribe to a folder
*
* @param string $folder folder name
* @return Response
*
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
*/
public function subscribeFolder(string $folder): Response;
/**
* Unsubscribe from a folder
*
* @param string $folder folder name
* @return Response
*
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
*/
public function unsubscribeFolder(string $folder): Response;
/**
* Send idle command
*
* @throws RuntimeException
*/
public function idle();
/**
* Send done command
* @throws RuntimeException
*/
public function done();
/**
* Apply session saved changes to the server
*
* @return Response
*
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
*/
public function expunge(): Response;
/**
* Retrieve the quota level settings, and usage statics per mailbox
* @param $username
*
* @return Response
* @throws RuntimeException
*/
public function getQuota($username): Response;
/**
* Retrieve the quota settings per user
*
* @param string $quota_root
*
* @return Response
* @throws ConnectionFailedException
*/
public function getQuotaRoot(string $quota_root = 'INBOX'): Response;
/**
* Send noop command
*
* @return Response
*
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
*/
public function noop(): Response;
/**
* Do a search request
*
* @param array $params
* @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use
* message numbers instead.
*
* @return Response containing the message ids
* @throws RuntimeException
*/
public function search(array $params, int|string $uid = IMAP::ST_UID): Response;
/**
* Get a message overview
* @param string $sequence uid sequence
* @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use
* message numbers instead.
*
* @return Response
* @throws RuntimeException
* @throws MessageNotFoundException
* @throws InvalidMessageDateException
*/
public function overview(string $sequence, int|string $uid = IMAP::ST_UID): Response;
/**
* Enable the debug mode
*/
public function enableDebug();
/**
* Disable the debug mode
*/
public function disableDebug();
/**
* Enable uid caching
*/
public function enableUidCache();
/**
* Disable uid caching
*/
public function disableUidCache();
/**
* Set the uid cache of current active folder
*
* @param array|null $uids
*/
public function setUidCache(?array $uids);
}

View File

@@ -0,0 +1,417 @@
<?php
/*
* File: Response.php
* Category: -
* Author: M.Goldenbaum
* Created: 30.12.22 19:46
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Connection\Protocols;
use Webklex\PHPIMAP\Exceptions\ResponseException;
/**
* Class Response
*
* @package Webklex\PHPIMAP\Connection\Protocols
*/
class Response {
/**
* The commands used to fetch or manipulate data
* @var array $command
*/
protected array $commands = [];
/**
* The original response received
* @var array $response
*/
protected array $response = [];
/**
* Errors that have occurred while fetching or parsing the response
* @var array $errors
*/
protected array $errors = [];
/**
* Result to be returned
* @var mixed|null $result
*/
protected mixed $result = null;
/**
* Noun to identify the request / response
* @var int $noun
*/
protected int $noun = 0;
/**
* Other related responses
* @var array $response_stack
*/
protected array $response_stack = [];
/**
* Debug flag
* @var bool $debug
*/
protected bool $debug = false;
/**
* Can the response be empty?
* @var bool $can_be_empty
*/
protected bool $can_be_empty = false;
/**
* Create a new Response instance
*/
public function __construct(int $noun, bool $debug = false) {
$this->debug = $debug;
$this->noun = $noun > 0 ? $noun : (int)str_replace(".", "", (string)microtime(true));
}
/**
* Make a new response instance
* @param int $noun
* @param array $commands
* @param array $responses
* @param bool $debug
*
* @return Response
*/
public static function make(int $noun, array $commands = [], array $responses = [], bool $debug = false): Response {
return (new self($noun, $debug))->setCommands($commands)->setResponse($responses);
}
/**
* Create a new empty response
* @param bool $debug
*
* @return Response
*/
public static function empty(bool $debug = false): Response {
return (new self(0, $debug));
}
/**
* Stack another response
* @param Response $response
*
* @return void
*/
public function stack(Response $response): void {
$this->response_stack[] = $response;
}
/**
* Get the associated response stack
*
* @return array
*/
public function getStack(): array {
return $this->response_stack;
}
/**
* Get all assigned commands
*
* @return array
*/
public function getCommands(): array {
return $this->commands;
}
/**
* Add a new command
* @param string $command
*
* @return Response
*/
public function addCommand(string $command): Response {
$this->commands[] = $command;
return $this;
}
/**
* Set and overwrite all commands
* @param array $commands
*
* @return Response
*/
public function setCommands(array $commands): Response {
$this->commands = $commands;
return $this;
}
/**
* Get all set errors
*
* @return array
*/
public function getErrors(): array {
$errors = $this->errors;
foreach($this->getStack() as $response) {
$errors = array_merge($errors, $response->getErrors());
}
return $errors;
}
/**
* Set and overwrite all existing errors
* @param array $errors
*
* @return Response
*/
public function setErrors(array $errors): Response {
$this->errors = $errors;
return $this;
}
/**
* Set the response
* @param string $error
*
* @return Response
*/
public function addError(string $error): Response {
$this->errors[] = $error;
return $this;
}
/**
* Set the response
* @param array $response
*
* @return Response
*/
public function addResponse(mixed $response): Response {
$this->response[] = $response;
return $this;
}
/**
* Set the response
* @param array $response
*
* @return Response
*/
public function setResponse(array $response): Response {
$this->response = $response;
return $this;
}
/**
* Get the assigned response
*
* @return array
*/
public function getResponse(): array {
return $this->response;
}
/**
* Set the result data
* @param mixed $result
*
* @return Response
*/
public function setResult(mixed $result): Response {
$this->result = $result;
return $this;
}
/**
* Wrap a result bearing action
* @param callable $callback
*
* @return Response
*/
public function wrap(callable $callback): Response {
$this->result = call_user_func($callback, $this);
return $this;
}
/**
* Get the response data
*
* @return mixed
*/
public function data(): mixed {
if ($this->result !== null) {
return $this->result;
}
return $this->getResponse();
}
/**
* Get the response data as array
*
* @return array
*/
public function array(): array {
$data = $this->data();
if(is_array($data)){
return $data;
}
return [$data];
}
/**
* Get the response data as string
*
* @return string
*/
public function string(): string {
$data = $this->data();
if(is_array($data)){
return implode(" ", $data);
}
return (string)$data;
}
/**
* Get the response data as integer
*
* @return int
*/
public function integer(): int {
$data = $this->data();
if(is_array($data) && isset($data[0])){
return (int)$data[0];
}
return (int)$data;
}
/**
* Get the response data as boolean
*
* @return bool
*/
public function boolean(): bool {
return (bool)$this->data();
}
/**
* Validate and retrieve the response data
*
* @throws ResponseException
*/
public function validatedData(): mixed {
return $this->validate()->data();
}
/**
* Validate the response date
*
* @throws ResponseException
*/
public function validate(): Response {
if ($this->failed()) {
throw ResponseException::make($this, $this->debug);
}
return $this;
}
/**
* Check if the Response can be considered successful
*
* @return bool
*/
public function successful(): bool {
foreach(array_merge($this->getResponse(), $this->array()) as $data) {
if (!$this->verify_data($data)) {
return false;
}
}
foreach($this->getStack() as $response) {
if (!$response->successful()) {
return false;
}
}
return ($this->boolean() || $this->canBeEmpty()) && !$this->getErrors();
}
/**
* Check if the Response can be considered failed
* @param mixed $data
*
* @return bool
*/
public function verify_data(mixed $data): bool {
if (is_array($data)) {
foreach ($data as $line) {
if (is_array($line)) {
if(!$this->verify_data($line)){
return false;
}
}else{
if (!$this->verify_line((string)$line)) {
return false;
}
}
}
}else{
if (!$this->verify_line((string)$data)) {
return false;
}
}
return true;
}
/**
* Verify a single line
* @param string $line
*
* @return bool
*/
public function verify_line(string $line): bool {
return !str_starts_with($line, "TAG".$this->noun." BAD ") && !str_starts_with($line, "TAG".$this->noun." NO ");
}
/**
* Check if the Response can be considered failed
*
* @return bool
*/
public function failed(): bool {
return !$this->successful();
}
/**
* Get the Response noun
*
* @return int
*/
public function Noun(): int {
return $this->noun;
}
/**
* Set the Response to be allowed to be empty
* @param bool $can_be_empty
*
* @return $this
*/
public function setCanBeEmpty(bool $can_be_empty): Response {
$this->can_be_empty = $can_be_empty;
return $this;
}
/**
* Check if the Response can be empty
*
* @return bool
*/
public function canBeEmpty(): bool {
return $this->can_be_empty;
}
}

View File

@@ -0,0 +1,591 @@
<?php
/*
* File: EncodingAliases.php
* Category: -
* Author: S. Todorov (https://github.com/todorowww)
* Created: 23.04.18 14:16
* Updated: -
*
* Description:
* Contains email encoding aliases, thta can occur when fetching emails. These sometimes can break icvon()
* This file attempts to correct this by using a list of aliases and their mappings to supported iconv() encodings
*/
namespace Webklex\PHPIMAP;
/**
* Class EncodingAliases
*
* @package Webklex\PHPIMAP
*/
class EncodingAliases {
/**
* Contains email encoding mappings
*
* @var array
*/
private static array $aliases = [
/*
|--------------------------------------------------------------------------
| Email encoding aliases
|--------------------------------------------------------------------------
|
| Email encoding aliases used to convert to iconv supported charsets
|
|
| This Source Code Form is subject to the terms of the Mozilla Public
| License, v. 2.0. If a copy of the MPL was not distributed with this
| file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
| This Original Code has been modified by IBM Corporation.
| Modifications made by IBM described herein are
| Copyright (c) International Business Machines
| Corporation, 1999
|
| Modifications to Mozilla code or documentation
| identified per MPL Section 3.3
|
| Date Modified by Description of modification
| 12/09/1999 IBM Corp. Support for IBM codepages - 850,852,855,857,862,864
|
| Rule of this file:
| 1. key should always be in lower case ascii so we can do case insensitive
| comparison in the code faster.
| 2. value should be the one used in unicode converter
|
| 3. If the charset is not used for document charset, but font charset
| (e.g. XLFD charset- such as JIS x0201, JIS x0208), don't put here
|
*/
"ascii" => "us-ascii",
"us-ascii" => "us-ascii",
"ansi_x3.4-1968" => "us-ascii",
"646" => "us-ascii",
"iso-8859-1" => "ISO-8859-1",
"iso-8859-2" => "ISO-8859-2",
"iso-8859-3" => "ISO-8859-3",
"iso-8859-4" => "ISO-8859-4",
"iso-8859-5" => "ISO-8859-5",
"iso-8859-6" => "ISO-8859-6",
"iso-8859-6-i" => "ISO-8859-6-I",
"iso-8859-6-e" => "ISO-8859-6-E",
"iso-8859-7" => "ISO-8859-7",
"iso-8859-8" => "ISO-8859-8",
"iso-8859-8-i" => "ISO-8859-8-I",
"iso-8859-8-e" => "ISO-8859-8-E",
"iso-8859-9" => "ISO-8859-9",
"iso-8859-10" => "ISO-8859-10",
"iso-8859-11" => "ISO-8859-11",
"iso-8859-13" => "ISO-8859-13",
"iso-8859-14" => "ISO-8859-14",
"iso-8859-15" => "ISO-8859-15",
"iso-8859-16" => "ISO-8859-16",
"iso-ir-111" => "ISO-IR-111",
"iso-2022-cn" => "ISO-2022-CN",
"iso-2022-cn-ext" => "ISO-2022-CN",
"iso-2022-kr" => "ISO-2022-KR",
"iso-2022-jp" => "ISO-2022-JP",
"utf-16be" => "UTF-16BE",
"utf-16le" => "UTF-16LE",
"utf-16" => "UTF-16",
"windows-1250" => "windows-1250",
"windows-1251" => "windows-1251",
"windows-1252" => "windows-1252",
"windows-1253" => "windows-1253",
"windows-1254" => "windows-1254",
"windows-1255" => "windows-1255",
"windows-1256" => "windows-1256",
"windows-1257" => "windows-1257",
"windows-1258" => "windows-1258",
"ibm866" => "IBM866",
"ibm850" => "IBM850",
"ibm852" => "IBM852",
"ibm855" => "IBM855",
"ibm857" => "IBM857",
"ibm862" => "IBM862",
"ibm864" => "IBM864",
"utf-8" => "UTF-8",
"utf-7" => "UTF-7",
"utf-7-imap" => "UTF7-IMAP",
"utf7-imap" => "UTF7-IMAP",
"shift_jis" => "Shift_JIS",
"big5" => "Big5",
"euc-jp" => "EUC-JP",
"euc-kr" => "EUC-KR",
"gb2312" => "GB2312",
"gb18030" => "gb18030",
"viscii" => "VISCII",
"koi8-r" => "KOI8-R",
"koi8_r" => "KOI8-R",
"cskoi8r" => "KOI8-R",
"koi" => "KOI8-R",
"koi8" => "KOI8-R",
"koi8-u" => "KOI8-U",
"tis-620" => "TIS-620",
"t.61-8bit" => "T.61-8bit",
"hz-gb-2312" => "HZ-GB-2312",
"big5-hkscs" => "Big5-HKSCS",
"gbk" => "gbk",
"cns11643" => "x-euc-tw",
//
// Aliases for ISO-8859-1
//
"latin1" => "ISO-8859-1",
"iso_8859-1" => "ISO-8859-1",
"iso8859-1" => "ISO-8859-1",
"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",
"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",
"iso_8859-1:1987" => "ISO-8859-1",
"iso-ir-100" => "ISO-8859-1",
"l1" => "ISO-8859-1",
"ibm819" => "ISO-8859-1",
"cp819" => "ISO-8859-1",
"csisolatin1" => "ISO-8859-1",
//
// Aliases for ISO-8859-2
//
"latin2" => "ISO-8859-2",
"iso_8859-2" => "ISO-8859-2",
"iso_8859-2:1987" => "ISO-8859-2",
"iso-ir-101" => "ISO-8859-2",
"l2" => "ISO-8859-2",
"csisolatin2" => "ISO-8859-2",
//
// Aliases for ISO-8859-3
//
"latin3" => "ISO-8859-3",
"iso_8859-3" => "ISO-8859-3",
"iso_8859-3:1988" => "ISO-8859-3",
"iso-ir-109" => "ISO-8859-3",
"l3" => "ISO-8859-3",
"csisolatin3" => "ISO-8859-3",
//
// Aliases for ISO-8859-4
//
"latin4" => "ISO-8859-4",
"iso_8859-4" => "ISO-8859-4",
"iso_8859-4:1988" => "ISO-8859-4",
"iso-ir-110" => "ISO-8859-4",
"l4" => "ISO-8859-4",
"csisolatin4" => "ISO-8859-4",
//
// Aliases for ISO-8859-5
//
"cyrillic" => "ISO-8859-5",
"iso_8859-5" => "ISO-8859-5",
"iso_8859-5:1988" => "ISO-8859-5",
"iso-ir-144" => "ISO-8859-5",
"csisolatincyrillic" => "ISO-8859-5",
//
// Aliases for ISO-8859-6
//
"arabic" => "ISO-8859-6",
"iso_8859-6" => "ISO-8859-6",
"iso_8859-6:1987" => "ISO-8859-6",
"iso-ir-127" => "ISO-8859-6",
"ecma-114" => "ISO-8859-6",
"asmo-708" => "ISO-8859-6",
"csisolatinarabic" => "ISO-8859-6",
//
// Aliases for ISO-8859-6-I
//
"csiso88596i" => "ISO-8859-6-I",
//
// Aliases for ISO-8859-6-E",
//
"csiso88596e" => "ISO-8859-6-E",
//
// Aliases for ISO-8859-7",
//
"greek" => "ISO-8859-7",
"greek8" => "ISO-8859-7",
"sun_eu_greek" => "ISO-8859-7",
"iso_8859-7" => "ISO-8859-7",
"iso_8859-7:1987" => "ISO-8859-7",
"iso-ir-126" => "ISO-8859-7",
"elot_928" => "ISO-8859-7",
"ecma-118" => "ISO-8859-7",
"csisolatingreek" => "ISO-8859-7",
//
// Aliases for ISO-8859-8",
//
"hebrew" => "ISO-8859-8",
"iso_8859-8" => "ISO-8859-8",
"visual" => "ISO-8859-8",
"iso_8859-8:1988" => "ISO-8859-8",
"iso-ir-138" => "ISO-8859-8",
"csisolatinhebrew" => "ISO-8859-8",
//
// Aliases for ISO-8859-8-I",
//
"csiso88598i" => "ISO-8859-8-I",
"iso-8859-8i" => "ISO-8859-8-I",
"logical" => "ISO-8859-8-I",
//
// Aliases for ISO-8859-8-E",
//
"csiso88598e" => "ISO-8859-8-E",
//
// Aliases for ISO-8859-9",
//
"latin5" => "ISO-8859-9",
"iso_8859-9" => "ISO-8859-9",
"iso_8859-9:1989" => "ISO-8859-9",
"iso-ir-148" => "ISO-8859-9",
"l5" => "ISO-8859-9",
"csisolatin5" => "ISO-8859-9",
//
// Aliases for UTF-8",
//
"unicode-1-1-utf-8" => "UTF-8",
// nl_langinfo(CODESET) in HP/UX returns 'utf8' under UTF-8 locales",
"utf8" => "UTF-8",
//
// Aliases for Shift_JIS",
//
"x-sjis" => "Shift_JIS",
"shift-jis" => "Shift_JIS",
"ms_kanji" => "Shift_JIS",
"csshiftjis" => "Shift_JIS",
"windows-31j" => "Shift_JIS",
"cp932" => "Shift_JIS",
"sjis" => "Shift_JIS",
//
// Aliases for EUC_JP",
//
"cseucpkdfmtjapanese" => "EUC-JP",
"x-euc-jp" => "EUC-JP",
//
// Aliases for ISO-2022-JP",
//
"csiso2022jp" => "ISO-2022-JP",
// The following are really not aliases ISO-2022-JP, but sharing the same decoder",
"iso-2022-jp-2" => "ISO-2022-JP",
"csiso2022jp2" => "ISO-2022-JP",
//
// Aliases for Big5",
//
"csbig5" => "Big5",
"cn-big5" => "Big5",
// x-x-big5 is not really a alias for Big5, add it only for MS FrontPage",
"x-x-big5" => "Big5",
// Sun Solaris",
"zh_tw-big5" => "Big5",
//
// Aliases for EUC-KR",
//
"cseuckr" => "EUC-KR",
"ks_c_5601-1987" => "EUC-KR",
"iso-ir-149" => "EUC-KR",
"ks_c_5601-1989" => "EUC-KR",
"ksc_5601" => "EUC-KR",
"ksc5601" => "EUC-KR",
"korean" => "EUC-KR",
"csksc56011987" => "EUC-KR",
"5601" => "EUC-KR",
"windows-949" => "EUC-KR",
//
// Aliases for GB2312",
//
// The following are really not aliases GB2312, add them only for MS FrontPage",
"gb_2312-80" => "GB2312",
"iso-ir-58" => "GB2312",
"chinese" => "GB2312",
"csiso58gb231280" => "GB2312",
"csgb2312" => "GB2312",
"zh_cn.euc" => "GB2312",
// Sun Solaris",
"gb_2312" => "GB2312",
//
// Aliases for windows-125x ",
//
"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",
//
// Aliases for windows-874 ",
//
"windows-874" => "windows-874",
"ibm874" => "windows-874",
"dos-874" => "windows-874",
//
// Aliases for macintosh",
//
"macintosh" => "macintosh",
"x-mac-roman" => "macintosh",
"mac" => "macintosh",
"csmacintosh" => "macintosh",
//
// Aliases for IBM866",
//
"cp866" => "IBM866",
"cp-866" => "IBM866",
"866" => "IBM866",
"csibm866" => "IBM866",
//
// Aliases for IBM850",
//
"cp850" => "IBM850",
"850" => "IBM850",
"csibm850" => "IBM850",
//
// Aliases for IBM852",
//
"cp852" => "IBM852",
"852" => "IBM852",
"csibm852" => "IBM852",
//
// Aliases for IBM855",
//
"cp855" => "IBM855",
"855" => "IBM855",
"csibm855" => "IBM855",
//
// Aliases for IBM857",
//
"cp857" => "IBM857",
"857" => "IBM857",
"csibm857" => "IBM857",
//
// Aliases for IBM862",
//
"cp862" => "IBM862",
"862" => "IBM862",
"csibm862" => "IBM862",
//
// Aliases for IBM864",
//
"cp864" => "IBM864",
"864" => "IBM864",
"csibm864" => "IBM864",
"ibm-864" => "IBM864",
//
// Aliases for T.61-8bit",
//
"t.61" => "T.61-8bit",
"iso-ir-103" => "T.61-8bit",
"csiso103t618bit" => "T.61-8bit",
//
// Aliases for UTF-7",
//
"x-unicode-2-0-utf-7" => "UTF-7",
"unicode-2-0-utf-7" => "UTF-7",
"unicode-1-1-utf-7" => "UTF-7",
"csunicode11utf7" => "UTF-7",
//
// Aliases for ISO-10646-UCS-2",
//
"csunicode" => "UTF-16BE",
"csunicode11" => "UTF-16BE",
"iso-10646-ucs-basic" => "UTF-16BE",
"csunicodeascii" => "UTF-16BE",
"iso-10646-unicode-latin1" => "UTF-16BE",
"csunicodelatin1" => "UTF-16BE",
"iso-10646" => "UTF-16BE",
"iso-10646-j-1" => "UTF-16BE",
//
// Aliases for ISO-8859-10",
//
"latin6" => "ISO-8859-10",
"iso-ir-157" => "ISO-8859-10",
"l6" => "ISO-8859-10",
// Currently .properties cannot handle : in key",
//iso_8859-10:1992" => "ISO-8859-10",
"csisolatin6" => "ISO-8859-10",
//
// Aliases for ISO-8859-15",
//
"iso_8859-15" => "ISO-8859-15",
"csisolatin9" => "ISO-8859-15",
"l9" => "ISO-8859-15",
//
// Aliases for ISO-IR-111",
//
"ecma-cyrillic" => "ISO-IR-111",
"csiso111ecmacyrillic" => "ISO-IR-111",
//
// Aliases for ISO-2022-KR",
//
"csiso2022kr" => "ISO-2022-KR",
//
// Aliases for VISCII",
//
"csviscii" => "VISCII",
//
// Aliases for x-euc-tw",
//
"zh_tw-euc" => "x-euc-tw",
//
// Following names appears in unix nl_langinfo(CODESET)",
// They can be compiled as platform specific if necessary",
// DONT put things here if it does not look generic enough (like hp15CN)",
//
"iso88591" => "ISO-8859-1",
"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",
"iso885910" => "ISO-8859-10",
"iso885911" => "ISO-8859-11",
"iso885912" => "ISO-8859-12",
"iso885913" => "ISO-8859-13",
"iso885914" => "ISO-8859-14",
"iso885915" => "ISO-8859-15",
"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",
"x-gbk" => "gbk",
"windows-936" => "gbk",
"ansi-1251" => "windows-1251",
];
/**
* Returns proper encoding mapping, if exists. If it doesn't, return unchanged $encoding
* @param string|null $encoding
* @param string|null $fallback
*
* @return string
*/
public static function get(?string $encoding, string $fallback = null): string {
if (isset(self::$aliases[strtolower($encoding ?? '')])) {
return self::$aliases[strtolower($encoding ?? '')];
}
return $fallback ?: $encoding;
}
/**
* Convert the encoding of a string
* @param $str
* @param string $from
* @param string $to
*
* @return mixed
*/
public static function convert($str, string $from = "ISO-8859-2", string $to = "UTF-8"): mixed {
$from = self::get($from, self::detectEncoding($str));
$to = self::get($to, self::detectEncoding($str));
if ($from === $to) {
return $str;
}
// We don't need to do convertEncoding() if charset is ASCII (us-ascii):
// ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded
// https://stackoverflow.com/a/11303410
//
// us-ascii is the same as ASCII:
// ASCII is the traditional name for the encoding system; the Internet Assigned Numbers Authority (IANA)
// prefers the updated name US-ASCII, which clarifies that this system was developed in the US and
// based on the typographical symbols predominantly in use there.
// https://en.wikipedia.org/wiki/ASCII
//
// convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken.
if (strtolower($from) == 'us-ascii' && $to == 'UTF-8') {
return $str;
}
try {
if (function_exists('iconv') && !self::isUtf7($from) && !self::isUtf7($to)) {
return iconv($from, $to, $str);
}
if (!$from) {
return mb_convert_encoding($str, $to);
}
return mb_convert_encoding($str, $to, $from);
} catch (\Exception $e) {
if (str_contains($from, '-')) {
$from = str_replace('-', '', $from);
return self::convert($str, $from, $to);
}
return $str;
}
}
/**
* Attempts to detect the encoding of a string
* @param string $string
*
* @return string
*/
public static function detectEncoding(string $string): string {
$encoding = mb_detect_encoding($string, array_filter(self::getEncodings(), function($value){
return !in_array($value, [
'ISO-8859-6-I', 'ISO-8859-6-E', 'ISO-8859-8-I', 'ISO-8859-8-E',
'ISO-8859-11', 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'ISO-8859-16', 'ISO-IR-111',"ISO-2022-CN",
"windows-1250", "windows-1253", "windows-1255", "windows-1256", "windows-1257", "windows-1258",
"IBM852", "IBM855", "IBM857", "IBM866", "IBM864", "IBM862", "KOI8-R", "KOI8-U",
"TIS-620", "ISO-8859-1", "ISO-8859-2", "ISO-8859-3", "ISO-8859-4",
"VISCII", "T.61-8bit", "Big5-HKSCS", "windows-874", "macintosh", "ISO-8859-12", "ISO-8859-7",
"IMAP-UTF-7"
]);
}), true);
if ($encoding === false) {
$encoding = 'UTF-8';
}
return $encoding;
}
/**
* Returns all available encodings
*
* @return array
*/
public static function getEncodings(): array {
$encodings = [];
foreach (self::$aliases as $encoding) {
if (!in_array($encoding, $encodings)) {
$encodings[] = $encoding;
}
}
return $encodings;
}
/**
* Returns true if the encoding is UTF-7 like
* @param string $encoding
*
* @return bool
*/
public static function isUtf7(string $encoding): bool {
return str_contains(str_replace("-", "", strtolower($encoding)), "utf7");
}
/**
* Check if an encoding is supported
* @param string $encoding
*
* @return bool
*/
public static function has(string $encoding): bool {
return isset(self::$aliases[strtolower($encoding)]);
}
}

View File

@@ -0,0 +1,28 @@
<?php
/*
* File: Event.php
* Category: Event
* Author: M. Goldenbaum
* Created: 25.11.20 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Events;
/**
* Class Event
*
* @package Webklex\PHPIMAP\Events
*/
abstract class Event {
/**
* Dispatch the event with the given arguments.
*/
public static function dispatch(): Event {
return new static(func_get_args());
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* File: FlagDeletedEvent.php
* Category: Event
* Author: M. Goldenbaum
* Created: 25.11.20 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Events;
/**
* Class FlagDeletedEvent
*
* @package Webklex\PHPIMAP\Events
*/
class FlagDeletedEvent extends FlagNewEvent {
}

View File

@@ -0,0 +1,40 @@
<?php
/*
* File: FlagNewEvent.php
* Category: Event
* Author: M. Goldenbaum
* Created: 25.11.20 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Events;
use Webklex\PHPIMAP\Message;
/**
* Class FlagNewEvent
*
* @package Webklex\PHPIMAP\Events
*/
class FlagNewEvent extends Event {
/** @var Message $message */
public Message $message;
/** @var string $flag */
public string $flag;
/**
* Create a new event instance.
* @var array $arguments
*
* @return void
*/
public function __construct(array $arguments) {
$this->message = $arguments[0];
$this->flag = $arguments[1];
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* File: FolderDeletedEvent.php
* Category: Event
* Author: M. Goldenbaum
* Created: 25.11.20 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Events;
/**
* Class FolderDeletedEvent
*
* @package Webklex\PHPIMAP\Events
*/
class FolderDeletedEvent extends FolderNewEvent {
}

View File

@@ -0,0 +1,40 @@
<?php
/*
* File: FolderMovedEvent.php
* Category: Event
* Author: M. Goldenbaum
* Created: 25.11.20 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Events;
use Webklex\PHPIMAP\Folder;
/**
* Class FolderMovedEvent
*
* @package Webklex\PHPIMAP\Events
*/
class FolderMovedEvent extends Event {
/** @var Folder $old_folder */
public Folder $old_folder;
/** @var Folder $new_folder */
public Folder $new_folder;
/**
* Create a new event instance.
* @var Folder[] $folders
*
* @return void
*/
public function __construct(array $folders) {
$this->old_folder = $folders[0];
$this->new_folder = $folders[1];
}
}

View File

@@ -0,0 +1,36 @@
<?php
/*
* File: FolderNewEvent.php
* Category: Event
* Author: M. Goldenbaum
* Created: 25.11.20 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Events;
use Webklex\PHPIMAP\Folder;
/**
* Class FolderNewEvent
*
* @package Webklex\PHPIMAP\Events
*/
class FolderNewEvent extends Event {
/** @var Folder $folder */
public Folder $folder;
/**
* Create a new event instance.
* @var Folder[] $folders
*
* @return void
*/
public function __construct(array $folders) {
$this->folder = $folders[0];
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* File: MessageCopiedEvent.php
* Category: Event
* Author: M. Goldenbaum
* Created: 25.11.20 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Events;
/**
* Class MessageCopiedEvent
*
* @package Webklex\PHPIMAP\Events
*/
class MessageCopiedEvent extends MessageMovedEvent {
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* File: MessageDeletedEvent.php
* Category: Event
* Author: M. Goldenbaum
* Created: 25.11.20 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Events;
/**
* Class MessageDeletedEvent
*
* @package Webklex\PHPIMAP\Events
*/
class MessageDeletedEvent extends MessageNewEvent {
}

View File

@@ -0,0 +1,40 @@
<?php
/*
* File: MessageMovedEvent.php
* Category: Event
* Author: M. Goldenbaum
* Created: 25.11.20 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Events;
use Webklex\PHPIMAP\Message;
/**
* Class MessageMovedEvent
*
* @package Webklex\PHPIMAP\Events
*/
class MessageMovedEvent extends Event {
/** @var Message $old_message */
public Message $old_message;
/** @var Message $new_message */
public Message $new_message;
/**
* Create a new event instance.
* @var Message[] $messages
*
* @return void
*/
public function __construct(array $messages) {
$this->old_message = $messages[0];
$this->new_message = $messages[1];
}
}

View File

@@ -0,0 +1,36 @@
<?php
/*
* File: MessageNewEvent.php
* Category: Event
* Author: M. Goldenbaum
* Created: 25.11.20 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Events;
use Webklex\PHPIMAP\Message;
/**
* Class MessageNewEvent
*
* @package Webklex\PHPIMAP\Events
*/
class MessageNewEvent extends Event {
/** @var Message $message */
public Message $message;
/**
* Create a new event instance.
* @var Message[] $messages
*
* @return void
*/
public function __construct(array $messages) {
$this->message = $messages[0];
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* File: MessageRestoredEvent.php
* Category: Event
* Author: M. Goldenbaum
* Created: 25.11.20 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Events;
/**
* Class MessageRestoredEvent
*
* @package Webklex\PHPIMAP\Events
*/
class MessageRestoredEvent extends MessageNewEvent {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: AuthFailedException.php
* Category: Exception
* Author: M. Goldenbaum
* Created: 19.01.17 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class AuthFailedException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class AuthFailedException extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: ConnectionFailedException.php
* Category: Exception
* Author: M. Goldenbaum
* Created: 19.01.17 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class ConnectionFailedException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class ConnectionFailedException extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: EventNotFoundException.php
* Category: Exception
* Author: M. Goldenbaum
* Created: 05.03.18 23:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class EventNotFoundException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class EventNotFoundException extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: FolderFetchingException.php
* Category: Exception
* Author: M. Goldenbaum
* Created: 05.03.18 23:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class FolderFetchingException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class FolderFetchingException extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: GetMessagesFailedException.php
* Category: Exception
* Author: M. Goldenbaum
* Created: 19.01.17 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class GetMessagesFailedException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class GetMessagesFailedException extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: ImapBadRequestException.php
* Category: Exception
* Author: S. Janaczek
* Created: 08.09.22 08:39
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class GetMessagesFailedException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class ImapBadRequestException extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: ImapServerErrorException.php
* Category: Exception
* Author: S. Janaczek
* Created: 08.09.22 08:39
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class GetMessagesFailedException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class ImapServerErrorException extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: InvalidMessageDateException.php
* Category: Exception
* Author: M. Goldenbaum
* Created: 10.03.19 04:31
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class InvalidMessageDateException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class InvalidMessageDateException extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: InvalidWhereQueryCriteriaException.php
* Category: Exception
* Author: M. Goldenbaum
* Created: 21.07.18 19:04
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class InvalidWhereQueryCriteriaException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class InvalidWhereQueryCriteriaException extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: MaskNotFoundException.php
* Category: Exception
* Author: M. Goldenbaum
* Created: 05.03.18 23:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class MaskNotFoundException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class MaskNotFoundException extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: MessageContentFetchingException.php
* Category: Exception
* Author: M. Goldenbaum
* Created: 05.03.18 23:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class MessageContentFetchingException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class MessageContentFetchingException extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: MessageFlagException.php
* Category: Exception
* Author: M. Goldenbaum
* Created: 02.01.21 02:47
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class MessageFlagException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class MessageFlagException extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: MessageHeaderFetchingException.php
* Category: Exception
* Author: M. Goldenbaum
* Created: 05.03.18 23:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class MessageHeaderFetchingException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class MessageHeaderFetchingException extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: MessageNotFoundException.php
* Category: Exception
* Author: M. Goldenbaum
* Created: 25.01.21 18:19
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class MessageNotFoundException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class MessageNotFoundException extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: MessageSearchValidationException.php
* Category: Exception
* Author: M. Goldenbaum
* Created: 05.03.18 23:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class MessageSearchValidationException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class MessageSearchValidationException extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: MessageSizeFetchingException.php
* Category: Exception
* Author: D. Malli
* Created: 24.02.23 17:55
* Updated: -
*
* Description: Exception thrown if fetching size for a message failed.
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class MessageSizeFetchingException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class MessageSizeFetchingException extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: MethodNotFoundException.php
* Category: Exception
* Author: M. Goldenbaum
* Created: 05.03.18 23:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class MethodNotFoundException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class MethodNotFoundException extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: MethodNotSupportedException.php
* Category: Exception
* Author: M. Goldenbaum
* Created: 05.03.18 23:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class MethodNotSupportedException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class MethodNotSupportedException extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: RuntimeException.php
* Category: Exception
* Author: M. Goldenbaum
* Created: 19.01.17 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class NotSupportedCapabilityException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class NotSupportedCapabilityException extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: ProtocolNotSupportedException.php
* Category: Exception
* Author: M. Goldenbaum
* Created: 19.01.17 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class ProtocolNotSupportedException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class ProtocolNotSupportedException extends Exception {
}

View File

@@ -0,0 +1,91 @@
<?php
/*
* File: ResponseException.php
* Category: Exception
* Author: M. Goldenbaum
* Created: 19.01.17 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
use Webklex\PHPIMAP\Connection\Protocols\Response;
/**
* Class ResponseException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class ResponseException extends Exception {
/**
* Make a new ResponseException instance
* @param Response $response
* @param false|boolean $debug
* @param Exception|null $exception
*
* @return ResponseException
*/
public static function make(Response $response, bool $debug = false, ?Exception $exception = null): ResponseException {
$message = "Command failed to process:\n";
$message .= "Causes:\n";
foreach($response->getErrors() as $error) {
$message .= "\t- $error\n";
}
if(!$response->data()) {
$message .= "\t- Empty response\n";
}
if ($debug) {
$message .= self::debug_message($response);
}
foreach($response->getStack() as $_response) {
$exception = self::make($_response, $debug, $exception);
}
return new self($message."Error occurred", 0, $exception);
}
/**
* Generate a debug message containing all commands send and responses received
* @param Response $response
*
* @return string
*/
protected static function debug_message(Response $response): string {
$commands = $response->getCommands();
$message = "Commands send:\n";
if ($commands) {
foreach($commands as $command) {
$message .= "\t".str_replace("\r\n", "\\r\\n", $command)."\n";
}
}else{
$message .= "\tNo command send!\n";
}
$responses = $response->getResponse();
$message .= "Responses received:\n";
if ($responses) {
foreach($responses as $_response) {
if (is_array($_response)) {
foreach($_response as $value) {
$message .= "\t".str_replace("\r\n", "\\r\\n", "$value")."\n";
}
}else{
$message .= "\t".str_replace("\r\n", "\\r\\n", "$_response")."\n";
}
}
}else{
$message .= "\tNo responses received!\n";
}
return $message;
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* File: RuntimeException.php
* Category: Exception
* Author: M. Goldenbaum
* Created: 19.01.17 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Exceptions;
use \Exception;
/**
* Class RuntimeException
*
* @package Webklex\PHPIMAP\Exceptions
*/
class RuntimeException extends Exception {
}

615
plugins/php-imap/src/Folder.php Executable file
View File

@@ -0,0 +1,615 @@
<?php
/*
* File: Folder.php
* Category: -
* Author: M. Goldenbaum
* Created: 19.01.17 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP;
use Carbon\Carbon;
use Webklex\PHPIMAP\Connection\Protocols\Response;
use Webklex\PHPIMAP\Exceptions\AuthFailedException;
use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
use Webklex\PHPIMAP\Exceptions\EventNotFoundException;
use Webklex\PHPIMAP\Exceptions\FolderFetchingException;
use Webklex\PHPIMAP\Exceptions\ImapBadRequestException;
use Webklex\PHPIMAP\Exceptions\ImapServerErrorException;
use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException;
use Webklex\PHPIMAP\Exceptions\MessageNotFoundException;
use Webklex\PHPIMAP\Exceptions\NotSupportedCapabilityException;
use Webklex\PHPIMAP\Exceptions\ResponseException;
use Webklex\PHPIMAP\Exceptions\RuntimeException;
use Webklex\PHPIMAP\Query\WhereQuery;
use Webklex\PHPIMAP\Support\FolderCollection;
use Webklex\PHPIMAP\Traits\HasEvents;
/**
* Class Folder
*
* @package Webklex\PHPIMAP
*/
class Folder {
use HasEvents;
/**
* Client instance
*
* @var Client
*/
protected Client $client;
/**
* Folder full path
*
* @var string
*/
public string $path;
/**
* Folder name
*
* @var string
*/
public string $name;
/**
* Folder full name
*
* @var string
*/
public string $full_name;
/**
* Children folders
*
* @var FolderCollection
*/
public FolderCollection $children;
/**
* Delimiter for folder
*
* @var string
*/
public string $delimiter;
/**
* Indicates if folder can't contain any "children".
* CreateFolder won't work on this folder.
*
* @var boolean
*/
public bool $no_inferiors;
/**
* Indicates if folder is only container, not a mailbox - you can't open it.
*
* @var boolean
*/
public bool $no_select;
/**
* Indicates if folder is marked. This means that it may contain new messages since the last time it was checked.
* Not provided by all IMAP servers.
*
* @var boolean
*/
public bool $marked;
/**
* Indicates if folder contains any "children".
* Not provided by all IMAP servers.
*
* @var boolean
*/
public bool $has_children;
/**
* Indicates if folder refers to others.
* Not provided by all IMAP servers.
*
* @var boolean
*/
public bool $referral;
/** @var array */
public array $status;
/** @var array */
public array $attributes = [];
const SPECIAL_ATTRIBUTES = [
'haschildren' => ['\haschildren'],
'hasnochildren' => ['\hasnochildren'],
'template' => ['\template', '\templates'],
'inbox' => ['\inbox'],
'sent' => ['\sent'],
'drafts' => ['\draft', '\drafts'],
'archive' => ['\archive', '\archives'],
'trash' => ['\trash'],
'junk' => ['\junk', '\spam'],
];
/**
* Folder constructor.
* @param Client $client
* @param string $folder_name
* @param string $delimiter
* @param string[] $attributes
*/
public function __construct(Client $client, string $folder_name, string $delimiter, array $attributes) {
$this->client = $client;
$this->events["message"] = $client->getDefaultEvents("message");
$this->events["folder"] = $client->getDefaultEvents("folder");
$this->setDelimiter($delimiter);
$this->path = $folder_name;
$this->full_name = $this->decodeName($folder_name);
$this->name = $this->getSimpleName($this->delimiter, $this->full_name);
$this->children = new FolderCollection();
$this->has_children = false;
$this->parseAttributes($attributes);
}
/**
* Get a new search query instance
* @param string[] $extensions
*
* @return WhereQuery
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws AuthFailedException
* @throws ConnectionFailedException
* @throws ResponseException
*/
public function query(array $extensions = []): WhereQuery {
$this->getClient()->checkConnection();
$this->getClient()->openFolder($this->path);
$extensions = count($extensions) > 0 ? $extensions : $this->getClient()->extensions;
return new WhereQuery($this->getClient(), $extensions);
}
/**
* Get a new search query instance
* @param string[] $extensions
*
* @return WhereQuery
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws AuthFailedException
* @throws ConnectionFailedException
* @throws ResponseException
*/
public function search(array $extensions = []): WhereQuery {
return $this->query($extensions);
}
/**
* Get a new search query instance
* @param string[] $extensions
*
* @return WhereQuery
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws AuthFailedException
* @throws ConnectionFailedException
* @throws ResponseException
*/
public function messages(array $extensions = []): WhereQuery {
return $this->query($extensions);
}
/**
* Determine if folder has children.
*
* @return bool
*/
public function hasChildren(): bool {
return $this->has_children;
}
/**
* Set children.
* @param FolderCollection $children
*
* @return Folder
*/
public function setChildren(FolderCollection $children): Folder {
$this->children = $children;
return $this;
}
/**
* Get children.
*
* @return FolderCollection
*/
public function getChildren(): FolderCollection {
return $this->children;
}
/**
* Decode name.
* It converts UTF7-IMAP encoding to UTF-8.
* @param $name
*
* @return string|array|bool|string[]|null
*/
protected function decodeName($name): string|array|bool|null {
$parts = [];
foreach(explode($this->delimiter, $name) as $item) {
$parts[] = EncodingAliases::convert($item, "UTF7-IMAP");
}
return implode($this->delimiter, $parts);
}
/**
* Get simple name (without parent folders).
* @param $delimiter
* @param $full_name
*
* @return string|bool
*/
protected function getSimpleName($delimiter, $full_name): string|bool {
$arr = explode($delimiter, $full_name);
return end($arr);
}
/**
* Parse attributes and set it to object properties.
* @param $attributes
*/
protected function parseAttributes($attributes): void {
$this->no_inferiors = in_array('\NoInferiors', $attributes);
$this->no_select = in_array('\NoSelect', $attributes);
$this->marked = in_array('\Marked', $attributes);
$this->referral = in_array('\Referral', $attributes);
$this->has_children = in_array('\HasChildren', $attributes);
array_map(function($el) {
foreach(self::SPECIAL_ATTRIBUTES as $key => $attribute) {
if(in_array(strtolower($el), $attribute)){
$this->attributes[] = $key;
}
}
}, $attributes);
}
/**
* Move or rename the current folder
* @param string $new_name
* @param boolean $expunge
*
* @return array
* @throws ConnectionFailedException
* @throws EventNotFoundException
* @throws FolderFetchingException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws AuthFailedException
* @throws ResponseException
*/
public function move(string $new_name, bool $expunge = true): array {
$this->client->checkConnection();
$status = $this->client->getConnection()->renameFolder($this->full_name, $new_name)->validatedData();
if($expunge) $this->client->expunge();
$folder = $this->client->getFolder($new_name);
$event = $this->getEvent("folder", "moved");
$event::dispatch($this, $folder);
return $status;
}
/**
* Get a message overview
* @param string|null $sequence uid sequence
*
* @return array
* @throws ConnectionFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws AuthFailedException
* @throws InvalidMessageDateException
* @throws MessageNotFoundException
* @throws ResponseException
*/
public function overview(string $sequence = null): array {
$this->client->openFolder($this->path);
$sequence = $sequence === null ? "1:*" : $sequence;
$uid = $this->client->getConfig()->get('options.sequence', IMAP::ST_MSGN);
$response = $this->client->getConnection()->overview($sequence, $uid);
return $response->validatedData();
}
/**
* Append a string message to the current mailbox
* @param string $message
* @param array|null $options
* @param string|Carbon|null $internal_date
*
* @return array
* @throws ConnectionFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws AuthFailedException
* @throws ResponseException
*/
public function appendMessage(string $message, array $options = null, Carbon|string $internal_date = null): array {
/**
* Check if $internal_date is parsed. If it is null it should not be set. Otherwise, the message can't be stored.
* If this parameter is set, it will set the INTERNALDATE on the appended message. The parameter should be a
* date string that conforms to the rfc2060 specifications for a date_time value or be a Carbon object.
*/
if($internal_date instanceof Carbon){
$internal_date = $internal_date->format('d-M-Y H:i:s O');
}
return $this->client->getConnection()->appendMessage($this->path, $message, $options, $internal_date)->validatedData();
}
/**
* Rename the current folder
* @param string $new_name
* @param boolean $expunge
*
* @return array
* @throws ConnectionFailedException
* @throws EventNotFoundException
* @throws FolderFetchingException
* @throws RuntimeException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws AuthFailedException
* @throws ResponseException
*/
public function rename(string $new_name, bool $expunge = true): array {
return $this->move($new_name, $expunge);
}
/**
* Delete the current folder
* @param boolean $expunge
*
* @return array
* @throws ConnectionFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws EventNotFoundException
* @throws AuthFailedException
* @throws ResponseException
*/
public function delete(bool $expunge = true): array {
$status = $this->client->getConnection()->deleteFolder($this->path)->validatedData();
if($this->client->getActiveFolder() == $this->path){
$this->client->setActiveFolder();
}
if($expunge) $this->client->expunge();
$event = $this->getEvent("folder", "deleted");
$event::dispatch($this);
return $status;
}
/**
* Subscribe the current folder
*
* @return array
* @throws ConnectionFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws AuthFailedException
* @throws ResponseException
*/
public function subscribe(): array {
$this->client->openFolder($this->path);
return $this->client->getConnection()->subscribeFolder($this->path)->validatedData();
}
/**
* Unsubscribe the current folder
*
* @return array
* @throws ConnectionFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws AuthFailedException
* @throws ResponseException
*/
public function unsubscribe(): array {
$this->client->openFolder($this->path);
return $this->client->getConnection()->unsubscribeFolder($this->path)->validatedData();
}
/**
* Idle the current connection
* @param callable $callback function(Message $message) gets called if a new message is received
* @param integer $timeout max 1740 seconds - recommended by rfc2177 §3. Should not be lower than the servers "* OK Still here" message interval
*
* @throws ConnectionFailedException
* @throws RuntimeException
* @throws AuthFailedException
* @throws NotSupportedCapabilityException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws ResponseException
*/
public function idle(callable $callback, int $timeout = 300): void {
$this->client->setTimeout($timeout);
if(!in_array("IDLE", $this->client->getConnection()->getCapabilities()->validatedData())){
throw new Exceptions\NotSupportedCapabilityException("IMAP server does not support IDLE");
}
$idle_client = $this->client->clone();
$idle_client->connect();
$idle_client->openFolder($this->path, true);
$idle_client->getConnection()->idle();
$last_action = Carbon::now()->addSeconds($timeout);
$sequence = $this->client->getConfig()->get('options.sequence', IMAP::ST_MSGN);
while(true) {
// This polymorphic call is fine - Protocol::idle() will throw an exception beforehand
$line = $idle_client->getConnection()->nextLine(Response::empty());
if(($pos = strpos($line, "EXISTS")) !== false){
$msgn = (int)substr($line, 2, $pos - 2);
// Check if the stream is still alive or should be considered stale
if(!$this->client->isConnected() || $last_action->isBefore(Carbon::now())){
// Reset the connection before interacting with it. Otherwise, the resource might be stale which
// would result in a stuck interaction. If you know of a way of detecting a stale resource, please
// feel free to improve this logic. I tried a lot but nothing seem to work reliably...
// Things that didn't work:
// - Closing the resource with fclose()
// - Verifying the resource with stream_get_meta_data()
// - Bool validating the resource stream (e.g.: (bool)$stream)
// - Sending a NOOP command
// - Sending a null package
// - Reading a null package
// - Catching the fs warning
// This polymorphic call is fine - Protocol::idle() will throw an exception beforehand
$this->client->getConnection()->reset();
// Establish a new connection
$this->client->connect();
}
$last_action = Carbon::now()->addSeconds($timeout);
// Always reopen the folder - otherwise the new message number isn't known to the current remote session
$this->client->openFolder($this->path, true);
$message = $this->query()->getMessageByMsgn($msgn);
$message->setSequence($sequence);
$callback($message);
$event = $this->getEvent("message", "new");
$event::dispatch($message);
}
}
}
/**
* Get folder status information from the EXAMINE command
*
* @return array
* @throws ConnectionFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws AuthFailedException
* @throws ResponseException
*/
public function status(): array {
return $this->client->getConnection()->folderStatus($this->path)->validatedData();
}
/**
* Get folder status information from the EXAMINE command
*
* @return array
* @throws AuthFailedException
* @throws ConnectionFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws ResponseException
* @throws RuntimeException
*
* @deprecated Use Folder::status() instead
*/
public function getStatus(): array {
return $this->status();
}
/**
* Load folder status information from the EXAMINE command
* @return Folder
* @throws AuthFailedException
* @throws ConnectionFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws ResponseException
* @throws RuntimeException
*/
public function loadStatus(): Folder {
$this->status = $this->examine();
return $this;
}
/**
* Examine the current folder
*
* @return array
* @throws ConnectionFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws AuthFailedException
* @throws ResponseException
*/
public function examine(): array {
return $this->client->getConnection()->examineFolder($this->path)->validatedData();
}
/**
* Select the current folder
*
* @return array
* @throws AuthFailedException
* @throws ConnectionFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws ResponseException
* @throws RuntimeException
*/
public function select(): array {
return $this->client->getConnection()->selectFolder($this->path)->validatedData();
}
/**
* Get the current Client instance
*
* @return Client
*/
public function getClient(): Client {
return $this->client;
}
/**
* Set the delimiter
* @param $delimiter
*/
public function setDelimiter($delimiter): void {
if(in_array($delimiter, [null, '', ' ', false]) === true){
$delimiter = $this->client->getConfig()->get('options.delimiter', '/');
}
$this->delimiter = $delimiter;
}
}

View File

@@ -0,0 +1,846 @@
<?php
/*
* File: Header.php
* Category: -
* Author: M.Goldenbaum
* Created: 17.09.20 20:38
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP;
use Carbon\Carbon;
use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException;
use Webklex\PHPIMAP\Exceptions\MethodNotFoundException;
/**
* Class Header
*
* @package Webklex\PHPIMAP
*/
class Header {
/**
* Raw header
*
* @var string $raw
*/
public string $raw = "";
/**
* Attribute holder
*
* @var Attribute[]|array $attributes
*/
protected array $attributes = [];
/**
* Config holder
*
* @var Config $config
*/
protected Config $config;
/**
* Config holder
*
* @var array $options
*/
protected array $options = [];
/**
* Fallback Encoding
*
* @var string
*/
public string $fallback_encoding = 'UTF-8';
/**
* Header constructor.
* @param Config $config
* @param string $raw_header
*
* @throws InvalidMessageDateException
*/
public function __construct(string $raw_header, Config $config) {
$this->raw = $raw_header;
$this->config = $config;
$this->options = $this->config->get('options');
$this->parse();
}
/**
* Call dynamic attribute setter and getter methods
* @param string $method
* @param array $arguments
*
* @return Attribute|mixed
* @throws MethodNotFoundException
*/
public function __call(string $method, array $arguments) {
if (strtolower(substr($method, 0, 3)) === 'get') {
$name = preg_replace('/(.)(?=[A-Z])/u', '$1_', substr(strtolower($method), 3));
if (in_array($name, array_keys($this->attributes))) {
return $this->attributes[$name];
}
}
throw new MethodNotFoundException("Method " . self::class . '::' . $method . '() is not supported');
}
/**
* Magic getter
* @param $name
*
* @return Attribute|null
*/
public function __get($name) {
return $this->get($name);
}
/**
* Get a specific header attribute
* @param $name
*
* @return Attribute
*/
public function get($name): Attribute {
$name = str_replace(["-", " "], "_", strtolower($name));
if (isset($this->attributes[$name])) {
return $this->attributes[$name];
}
return new Attribute($name);
}
/**
* Check if a specific attribute exists
* @param string $name
*
* @return bool
*/
public function has(string $name): bool {
$name = str_replace(["-", " "], "_", strtolower($name));
return isset($this->attributes[$name]);
}
/**
* Set a specific attribute
* @param string $name
* @param array|mixed $value
* @param boolean $strict
*
* @return Attribute|array
*/
public function set(string $name, mixed $value, bool $strict = false): Attribute|array {
if (isset($this->attributes[$name]) && $strict === false) {
$this->attributes[$name]->add($value, true);
} else {
$this->attributes[$name] = new Attribute($name, $value);
}
return $this->attributes[$name];
}
/**
* Perform a regex match all on the raw header and return the first result
* @param $pattern
*
* @return mixed|null
*/
public function find($pattern): mixed {
if (preg_match_all($pattern, $this->raw, $matches)) {
if (isset($matches[1])) {
if (count($matches[1]) > 0) {
return $matches[1][0];
}
}
}
return null;
}
/**
* Try to find a boundary if possible
*
* @return string|null
*/
public function getBoundary(): ?string {
$regex = $this->options["boundary"] ?? "/boundary=(.*?(?=;)|(.*))/i";
$boundary = $this->find($regex);
if ($boundary === null) {
return null;
}
return $this->clearBoundaryString($boundary);
}
/**
* Remove all unwanted chars from a given boundary
* @param string $str
*
* @return string
*/
private function clearBoundaryString(string $str): string {
return str_replace(['"', '\r', '\n', "\n", "\r", ";", "\s"], "", $str);
}
/**
* Parse the raw headers
*
* @throws InvalidMessageDateException
*/
protected function parse(): void {
$header = $this->rfc822_parse_headers($this->raw);
$this->extractAddresses($header);
if (property_exists($header, 'subject')) {
$this->set("subject", $this->decode($header->subject));
}
if (property_exists($header, 'references')) {
$this->set("references", array_map(function($item) {
return str_replace(['<', '>'], '', $item);
}, explode(" ", $header->references)));
}
if (property_exists($header, 'message_id')) {
$this->set("message_id", str_replace(['<', '>'], '', $header->message_id));
}
if (property_exists($header, 'in_reply_to')) {
$this->set("in_reply_to", str_replace(['<', '>'], '', $header->in_reply_to));
}
$this->parseDate($header);
foreach ($header as $key => $value) {
$key = trim(rtrim(strtolower($key)));
if (!isset($this->attributes[$key])) {
$this->set($key, $value);
}
}
$this->extractHeaderExtensions();
$this->findPriority();
}
/**
* Parse mail headers from a string
* @link https://php.net/manual/en/function.imap-rfc822-parse-headers.php
* @param $raw_headers
*
* @return object
*/
public function rfc822_parse_headers($raw_headers): object {
$headers = [];
$imap_headers = [];
if (extension_loaded('imap') && $this->options["rfc822"]) {
$raw_imap_headers = (array)\imap_rfc822_parse_headers($raw_headers);
foreach ($raw_imap_headers as $key => $values) {
$key = strtolower(str_replace("-", "_", $key));
$imap_headers[$key] = $values;
}
}
$lines = explode("\r\n", preg_replace("/\r\n\s/", ' ', $raw_headers));
$prev_header = null;
foreach ($lines as $line) {
if (str_starts_with($line, "\n")) {
$line = substr($line, 1);
}
if (str_starts_with($line, "\t")) {
$line = substr($line, 1);
$line = trim(rtrim($line));
if ($prev_header !== null) {
$headers[$prev_header][] = $line;
}
} elseif (str_starts_with($line, " ")) {
$line = substr($line, 1);
$line = trim(rtrim($line));
if ($prev_header !== null) {
if (!isset($headers[$prev_header])) {
$headers[$prev_header] = "";
}
if (is_array($headers[$prev_header])) {
$headers[$prev_header][] = $line;
} else {
$headers[$prev_header] .= $line;
}
}
} else {
if (($pos = strpos($line, ":")) > 0) {
$key = trim(rtrim(strtolower(substr($line, 0, $pos))));
$key = strtolower(str_replace("-", "_", $key));
$value = trim(rtrim(substr($line, $pos + 1)));
if (isset($headers[$key])) {
$headers[$key][] = $value;
} else {
$headers[$key] = [$value];
}
$prev_header = $key;
}
}
}
foreach ($headers as $key => $values) {
if (isset($imap_headers[$key])) {
continue;
}
$value = null;
switch ((string)$key) {
case 'from':
case 'to':
case 'cc':
case 'bcc':
case 'reply_to':
case 'sender':
$value = $this->decodeAddresses($values);
$headers[$key . "address"] = implode(", ", $values);
break;
case 'subject':
$value = implode(" ", $values);
break;
default:
if (is_array($values)) {
foreach ($values as $k => $v) {
if ($v == "") {
unset($values[$k]);
}
}
$available_values = count($values);
if ($available_values === 1) {
$value = array_pop($values);
} elseif ($available_values === 2) {
$value = implode(" ", $values);
} elseif ($available_values > 2) {
$value = array_values($values);
} else {
$value = "";
}
}
break;
}
$headers[$key] = $value;
}
return (object)array_merge($headers, $imap_headers);
}
/**
* Decode MIME header elements
* @link https://php.net/manual/en/function.imap-mime-header-decode.php
* @param string $text The MIME text
*
* @return array The decoded elements are returned in an array of objects, where each
* object has two properties, charset and text.
*/
public function mime_header_decode(string $text): array {
if (extension_loaded('imap')) {
$result = \imap_mime_header_decode($text);
return is_array($result) ? $result : [];
}
$charset = $this->getEncoding($text);
return [(object)[
"charset" => $charset,
"text" => $this->convertEncoding($text, $charset)
]];
}
/**
* Check if a given pair of strings has been decoded
* @param $encoded
* @param $decoded
*
* @return bool
*/
private function notDecoded($encoded, $decoded): bool {
return str_starts_with($decoded, '=?')
&& strlen($decoded) - 2 === strpos($decoded, '?=')
&& str_contains($encoded, $decoded);
}
/**
* Convert the encoding
* @param $str
* @param string $from
* @param string $to
*
* @return mixed|string
*/
public function convertEncoding($str, string $from = "ISO-8859-2", string $to = "UTF-8"): mixed {
$from = EncodingAliases::get($from, $this->fallback_encoding);
$to = EncodingAliases::get($to, $this->fallback_encoding);
if ($from === $to) {
return $str;
}
return EncodingAliases::convert($str, $from, $to);
}
/**
* Get the encoding of a given abject
* @param object|string $structure
*
* @return string
*/
public function getEncoding(object|string $structure): string {
if (property_exists($structure, 'parameters')) {
foreach ($structure->parameters as $parameter) {
if (strtolower($parameter->attribute) == "charset") {
return EncodingAliases::get($parameter->value, $this->fallback_encoding);
}
}
} elseif (property_exists($structure, 'charset')) {
return EncodingAliases::get($structure->charset, $this->fallback_encoding);
} elseif (is_string($structure) === true) {
$result = mb_detect_encoding($structure);
return $result === false ? $this->fallback_encoding : $result;
}
return $this->fallback_encoding;
}
/**
* Test if a given value is utf-8 encoded
* @param $value
*
* @return bool
*/
private function is_uft8($value): bool {
return str_starts_with(strtolower($value), '=?utf-8?');
}
/**
* Try to decode a specific header
* @param mixed $value
*
* @return mixed
*/
public function decode(mixed $value): mixed {
if (is_array($value)) {
return $this->decodeArray($value);
}
$original_value = $value;
$decoder = $this->options['decoder']['message'];
if ($value !== null) {
if ($decoder === 'utf-8') {
$decoded_values = $this->mime_header_decode($value);
$tempValue = "";
foreach ($decoded_values as $decoded_value) {
$tempValue .= $this->convertEncoding($decoded_value->text, $decoded_value->charset);
}
if ($tempValue) {
$value = $tempValue;
} else if (extension_loaded('imap')) {
$value = \imap_utf8($value);
} else if (function_exists('iconv_mime_decode')) {
$value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8");
} else {
$value = mb_decode_mimeheader($value);
}
} elseif ($decoder === 'iconv') {
$value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8");
} else if ($this->is_uft8($value)) {
$value = mb_decode_mimeheader($value);
}
if ($this->notDecoded($original_value, $value)) {
$value = $this->convertEncoding($original_value, $this->getEncoding($original_value));
}
}
return $value;
}
/**
* Decode a given array
* @param array $values
*
* @return array
*/
private function decodeArray(array $values): array {
foreach ($values as $key => $value) {
$values[$key] = $this->decode($value);
}
return $values;
}
/**
* Try to extract the priority from a given raw header string
*/
private function findPriority(): void {
$priority = $this->get("x_priority");
$priority = match ((int)"$priority") {
IMAP::MESSAGE_PRIORITY_HIGHEST => IMAP::MESSAGE_PRIORITY_HIGHEST,
IMAP::MESSAGE_PRIORITY_HIGH => IMAP::MESSAGE_PRIORITY_HIGH,
IMAP::MESSAGE_PRIORITY_NORMAL => IMAP::MESSAGE_PRIORITY_NORMAL,
IMAP::MESSAGE_PRIORITY_LOW => IMAP::MESSAGE_PRIORITY_LOW,
IMAP::MESSAGE_PRIORITY_LOWEST => IMAP::MESSAGE_PRIORITY_LOWEST,
default => IMAP::MESSAGE_PRIORITY_UNKNOWN,
};
$this->set("priority", $priority);
}
/**
* Extract a given part as address array from a given header
* @param $values
*
* @return array
*/
private function decodeAddresses($values): array {
$addresses = [];
if (extension_loaded('mailparse') && $this->options["rfc822"]) {
foreach ($values as $address) {
foreach (\mailparse_rfc822_parse_addresses($address) as $parsed_address) {
if (isset($parsed_address['address'])) {
$mail_address = explode('@', $parsed_address['address']);
if (count($mail_address) == 2) {
$addresses[] = (object)[
"personal" => $parsed_address['display'] ?? '',
"mailbox" => $mail_address[0],
"host" => $mail_address[1],
];
}
}
}
}
return $addresses;
}
foreach ($values as $address) {
foreach (preg_split('/, ?(?=(?:[^"]*"[^"]*")*[^"]*$)/', $address) as $split_address) {
$split_address = trim(rtrim($split_address));
if (strpos($split_address, ",") == strlen($split_address) - 1) {
$split_address = substr($split_address, 0, -1);
}
if (preg_match(
'/^(?:(?P<name>.+)\s)?(?(name)<|<?)(?P<email>[^\s]+?)(?(name)>|>?)$/',
$split_address,
$matches
)) {
$name = trim(rtrim($matches["name"]));
$email = trim(rtrim($matches["email"]));
list($mailbox, $host) = array_pad(explode("@", $email), 2, null);
$addresses[] = (object)[
"personal" => $name,
"mailbox" => $mailbox,
"host" => $host,
];
}
}
}
return $addresses;
}
/**
* Extract a given part as address array from a given header
* @param object $header
*/
private function extractAddresses(object $header): void {
foreach (['from', 'to', 'cc', 'bcc', 'reply_to', 'sender'] as $key) {
if (property_exists($header, $key)) {
$this->set($key, $this->parseAddresses($header->$key));
}
}
}
/**
* Parse Addresses
* @param $list
*
* @return array
*/
private function parseAddresses($list): array {
$addresses = [];
if (is_array($list) === false) {
return $addresses;
}
foreach ($list as $item) {
$address = (object)$item;
if (!property_exists($address, 'mailbox')) {
$address->mailbox = false;
}
if (!property_exists($address, 'host')) {
$address->host = false;
}
if (!property_exists($address, 'personal')) {
$address->personal = false;
} else {
$personalParts = $this->mime_header_decode($address->personal);
$address->personal = '';
foreach ($personalParts as $p) {
$address->personal .= $this->convertEncoding($p->text, $this->getEncoding($p));
}
if (str_starts_with($address->personal, "'")) {
$address->personal = str_replace("'", "", $address->personal);
}
}
if ($address->host == ".SYNTAX-ERROR.") {
$address->host = "";
}
if ($address->mailbox == "UNEXPECTED_DATA_AFTER_ADDRESS") {
$address->mailbox = "";
}
$address->mail = ($address->mailbox && $address->host) ? $address->mailbox . '@' . $address->host : false;
$address->full = ($address->personal) ? $address->personal . ' <' . $address->mail . '>' : $address->mail;
$addresses[] = new Address($address);
}
return $addresses;
}
/**
* Search and extract potential header extensions
*/
private function extractHeaderExtensions(): void {
foreach ($this->attributes as $key => $value) {
if (is_array($value)) {
$value = implode(", ", $value);
} else {
$value = (string)$value;
}
// Only parse strings and don't parse any attributes like the user-agent
if (!in_array($key, ["user-agent", "subject"])) {
if (($pos = strpos($value, ";")) !== false) {
$original = substr($value, 0, $pos);
$this->set($key, trim(rtrim($original)));
// Get all potential extensions
$extensions = explode(";", substr($value, $pos + 1));
$previousKey = null;
$previousValue = '';
foreach ($extensions as $extension) {
if (($pos = strpos($extension, "=")) !== false) {
$key = substr($extension, 0, $pos);
$key = trim(rtrim(strtolower($key)));
$matches = [];
if (preg_match('/^(?P<key_name>\w+)\*/', $key, $matches) !== 0) {
$key = $matches['key_name'];
$previousKey = $key;
$value = substr($extension, $pos + 1);
$value = str_replace('"', "", $value);
$previousValue .= trim(rtrim($value));
continue;
}
if (
$previousKey !== null
&& $previousKey !== $key
&& isset($this->attributes[$previousKey]) === false
) {
$this->set($previousKey, $previousValue);
$previousValue = '';
}
if (isset($this->attributes[$key]) === false) {
$value = substr($extension, $pos + 1);
$value = str_replace('"', "", $value);
$value = trim(rtrim($value));
$this->set($key, $value);
}
$previousKey = $key;
}
}
if ($previousValue !== '') {
$this->set($previousKey, $previousValue);
}
}
}
}
}
/**
* Exception handling for invalid dates
*
* Known bad and "invalid" formats:
* ^ Datetime ^ Problem ^ Cause
* | Mon, 20 Nov 2017 20:31:31 +0800 (GMT+8:00) | Double timezone specification | A Windows feature
* | Thu, 8 Nov 2018 08:54:58 -0200 (-02) |
* | | and invalid timezone (max 6 char) |
* | 04 Jan 2018 10:12:47 UT | Missing letter "C" | Unknown
* | Thu, 31 May 2018 18:15:00 +0800 (added by) | Non-standard details added by the | Unknown
* | | mail server |
* | Sat, 31 Aug 2013 20:08:23 +0580 | Invalid timezone | PHPMailer bug https://sourceforge.net/p/phpmailer/mailman/message/6132703/
*
* Please report any new invalid timestamps to [#45](https://github.com/Webklex/php-imap/issues)
*
* @param object $header
*
* @throws InvalidMessageDateException
*/
private function parseDate(object $header): void {
if (property_exists($header, 'date')) {
$date = $header->date;
if (preg_match('/\+0580/', $date)) {
$date = str_replace('+0580', '+0530', $date);
}
$date = trim(rtrim($date));
try {
if (str_contains($date, '&nbsp;')) {
$date = str_replace('&nbsp;', ' ', $date);
}
if (str_contains($date, ' UT ')) {
$date = str_replace(' UT ', ' UTC ', $date);
}
$parsed_date = Carbon::parse($date);
} catch (\Exception $e) {
switch (true) {
case preg_match('/([0-9]{4}\.[0-9]{1,2}\.[0-9]{1,2}\-[0-9]{1,2}\.[0-9]{1,2}.[0-9]{1,2})+$/i', $date) > 0:
$date = Carbon::createFromFormat("Y.m.d-H.i.s", $date);
break;
case preg_match('/([0-9]{2} [A-Z]{3} [0-9]{4} [0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2} [+-][0-9]{1,4} [0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2} [+-][0-9]{1,4})+$/i', $date) > 0:
$parts = explode(' ', $date);
array_splice($parts, -2);
$date = implode(' ', $parts);
break;
case preg_match('/([A-Z]{2,4}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4})+$/i', $date) > 0:
$array = explode(',', $date);
array_shift($array);
$date = Carbon::createFromFormat("d M Y H:i:s O", trim(implode(',', $array)));
break;
case preg_match('/([0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0:
case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ ([0-9]{2}|[0-9]{4})\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0:
$date .= 'C';
break;
case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}[\,]\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4})+$/i', $date) > 0:
$date = str_replace(',', '', $date);
break;
// match case for: Di., 15 Feb. 2022 06:52:44 +0100 (MEZ)/Di., 15 Feb. 2022 06:52:44 +0100 (MEZ)
case preg_match('/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \([A-Z]{3,4}\))\/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \([A-Z]{3,4}\))+$/i', $date) > 0:
$dates = explode('/', $date);
$date = array_shift($dates);
$array = explode(',', $date);
array_shift($array);
$date = trim(implode(',', $array));
$array = explode(' ', $date);
array_pop($array);
$date = trim(implode(' ', $array));
$date = Carbon::createFromFormat("d M. Y H:i:s O", $date);
break;
// match case for: fr., 25 nov. 2022 06:27:14 +0100/fr., 25 nov. 2022 06:27:14 +0100
case preg_match('/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4})\/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4})+$/i', $date) > 0:
$dates = explode('/', $date);
$date = array_shift($dates);
$array = explode(',', $date);
array_shift($array);
$date = trim(implode(',', $array));
$date = Carbon::createFromFormat("d M. Y H:i:s O", $date);
break;
case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ \+[0-9]{2,4}\ \(\+[0-9]{1,2}\))+$/i', $date) > 0:
case preg_match('/([A-Z]{2,3}[\,|\ \,]\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}.*)+$/i', $date) > 0:
case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \(.*)\)+$/i', $date) > 0:
case preg_match('/([A-Z]{2,3}\, \ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \(.*)\)+$/i', $date) > 0:
case preg_match('/([0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{2,4}\ [0-9]{2}\:[0-9]{2}\:[0-9]{2}\ [A-Z]{2}\ \-[0-9]{2}\:[0-9]{2}\ \([A-Z]{2,3}\ \-[0-9]{2}:[0-9]{2}\))+$/i', $date) > 0:
$array = explode('(', $date);
$array = array_reverse($array);
$date = trim(array_pop($array));
break;
}
try {
$parsed_date = Carbon::parse($date);
} catch (\Exception $_e) {
if (!isset($this->options["fallback_date"])) {
throw new InvalidMessageDateException("Invalid message date. ID:" . $this->get("message_id") . " Date:" . $header->date . "/" . $date, 1100, $e);
} else {
$parsed_date = Carbon::parse($this->options["fallback_date"]);
}
}
}
$this->set("date", $parsed_date);
}
}
/**
* Get all available attributes
*
* @return array
*/
public function getAttributes(): array {
return $this->attributes;
}
/**
* Set all header attributes
* @param array $attributes
*
* @return Header
*/
public function setAttributes(array $attributes): Header {
$this->attributes = $attributes;
return $this;
}
/**
* Set the configuration used for parsing a raw header
* @param array $config
*
* @return Header
*/
public function setOptions(array $config): Header {
$this->options = $config;
return $this;
}
/**
* Get the configuration used for parsing a raw header
*
* @return array
*/
public function getOptions(): array {
return $this->options;
}
/**
* Set the configuration used for parsing a raw header
* @param Config $config
*
* @return Header
*/
public function setConfig(Config $config): Header {
$this->config = $config;
return $this;
}
/**
* Get the configuration used for parsing a raw header
*
* @return Config
*/
public function getConfig(): Config {
return $this->config;
}
}

View File

@@ -0,0 +1,375 @@
<?php
/*
* File: IMAP.php
* Category: -
* Author: M.Goldenbaum
* Created: 14.03.19 18:22
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP;
/**
* Class IMAP
*
* Independent imap const holder
*/
class IMAP {
/**
* Message const
*
* @const integer TYPE_TEXT
* @const integer TYPE_MULTIPART
*
* @const integer ENC_7BIT
* @const integer ENC_8BIT
* @const integer ENC_BINARY
* @const integer ENC_BASE64
* @const integer ENC_QUOTED_PRINTABLE
* @const integer ENC_OTHER
*/
const MESSAGE_TYPE_TEXT = 0;
const MESSAGE_TYPE_MULTIPART = 1;
const MESSAGE_ENC_7BIT = 0;
const MESSAGE_ENC_8BIT = 1;
const MESSAGE_ENC_BINARY = 2;
const MESSAGE_ENC_BASE64 = 3;
const MESSAGE_ENC_QUOTED_PRINTABLE = 4;
const MESSAGE_ENC_OTHER = 5;
const MESSAGE_PRIORITY_UNKNOWN = 0;
const MESSAGE_PRIORITY_HIGHEST = 1;
const MESSAGE_PRIORITY_HIGH = 2;
const MESSAGE_PRIORITY_NORMAL = 3;
const MESSAGE_PRIORITY_LOW = 4;
const MESSAGE_PRIORITY_LOWEST = 5;
/**
* Attachment const
*
* @const integer TYPE_TEXT
* @const integer TYPE_MULTIPART
* @const integer TYPE_MESSAGE
* @const integer TYPE_APPLICATION
* @const integer TYPE_AUDIO
* @const integer TYPE_IMAGE
* @const integer TYPE_VIDEO
* @const integer TYPE_MODEL
* @const integer TYPE_OTHER
*/
const ATTACHMENT_TYPE_TEXT = 0;
const ATTACHMENT_TYPE_MULTIPART = 1;
const ATTACHMENT_TYPE_MESSAGE = 2;
const ATTACHMENT_TYPE_APPLICATION = 3;
const ATTACHMENT_TYPE_AUDIO = 4;
const ATTACHMENT_TYPE_IMAGE = 5;
const ATTACHMENT_TYPE_VIDEO = 6;
const ATTACHMENT_TYPE_MODEL = 7;
const ATTACHMENT_TYPE_OTHER = 8;
/**
* Client const
*
* @const integer CLIENT_OPENTIMEOUT
* @const integer CLIENT_READTIMEOUT
* @const integer CLIENT_WRITETIMEOUT
* @const integer CLIENT_CLOSETIMEOUT
*/
const CLIENT_OPENTIMEOUT = 1;
const CLIENT_READTIMEOUT = 2;
const CLIENT_WRITETIMEOUT = 3;
const CLIENT_CLOSETIMEOUT = 4;
/**
* Generic imap const
*
* @const integer NIL
* @const integer IMAP_OPENTIMEOUT
* @const integer IMAP_READTIMEOUT
* @const integer IMAP_WRITETIMEOUT
* @const integer IMAP_CLOSETIMEOUT
* @const integer OP_DEBUG
* @const integer OP_READONLY
* @const integer OP_ANONYMOUS
* @const integer OP_SHORTCACHE
* @const integer OP_SILENT
* @const integer OP_PROTOTYPE
* @const integer OP_HALFOPEN
* @const integer OP_EXPUNGE
* @const integer OP_SECURE
* @const integer CL_EXPUNGE
* @const integer FT_UID
* @const integer FT_PEEK
* @const integer FT_NOT
* @const integer FT_INTERNAL
* @const integer FT_PREFETCHTEXT
* @const integer ST_UID
* @const integer ST_SILENT
* @const integer ST_SET
* @const integer CP_UID
* @const integer CP_MOVE
* @const integer SE_UID
* @const integer SE_FREE
* @const integer SE_NOPREFETCH
* @const integer SO_FREE
* @const integer SO_NOSERVER
* @const integer SA_MESSAGES
* @const integer SA_RECENT
* @const integer SA_UNSEEN
* @const integer SA_UIDNEXT
* @const integer SA_UIDVALIDITY
* @const integer SA_ALL
* @const integer LATT_NOINFERIORS
* @const integer LATT_NOSELECT
* @const integer LATT_MARKED
* @const integer LATT_UNMARKED
* @const integer LATT_REFERRAL
* @const integer LATT_HASCHILDREN
* @const integer LATT_HASNOCHILDREN
* @const integer SORTDATE
* @const integer SORTARRIVAL
* @const integer SORTFROM
* @const integer SORTSUBJECT
* @const integer SORTTO
* @const integer SORTCC
* @const integer SORTSIZE
* @const integer TYPETEXT
* @const integer TYPEMULTIPART
* @const integer TYPEMESSAGE
* @const integer TYPEAPPLICATION
* @const integer TYPEAUDIO
* @const integer TYPEIMAGE
* @const integer TYPEVIDEO
* @const integer TYPEMODEL
* @const integer TYPEOTHER
* @const integer ENC7BIT
* @const integer ENC8BIT
* @const integer ENCBINARY
* @const integer ENCBASE64
* @const integer ENCQUOTEDPRINTABLE
* @const integer ENCOTHER
* @const integer IMAP_GC_ELT
* @const integer IMAP_GC_ENV
* @const integer IMAP_GC_TEXTS
*/
const NIL = 0;
const IMAP_OPENTIMEOUT = 1;
const IMAP_READTIMEOUT = 2;
const IMAP_WRITETIMEOUT = 3;
const IMAP_CLOSETIMEOUT = 4;
const OP_DEBUG = 1;
/**
* Open mailbox read-only
* @link http://php.net/manual/en/imap.constants.php
*/
const OP_READONLY = 2;
/**
* Don't use or update a .newsrc for news
* (NNTP only)
* @link http://php.net/manual/en/imap.constants.php
*/
const OP_ANONYMOUS = 4;
const OP_SHORTCACHE = 8;
const OP_SILENT = 16;
const OP_PROTOTYPE = 32;
/**
* For IMAP and NNTP
* names, open a connection but don't open a mailbox.
* @link http://php.net/manual/en/imap.constants.php
*/
const OP_HALFOPEN = 64;
const OP_EXPUNGE = 128;
const OP_SECURE = 256;
/**
* silently expunge the mailbox before closing when
* calling <b>imap_close</b>
* @link http://php.net/manual/en/imap.constants.php
*/
const CL_EXPUNGE = 32768;
/**
* The parameter is a UID
* @link http://php.net/manual/en/imap.constants.php
*/
const FT_UID = 1;
/**
* Do not set the \Seen flag if not already set
* @link http://php.net/manual/en/imap.constants.php
*/
const FT_PEEK = 2;
const FT_NOT = 4;
/**
* The return string is in internal format, will not canonicalize to CRLF.
* @link http://php.net/manual/en/imap.constants.php
*/
const FT_INTERNAL = 8;
const FT_PREFETCHTEXT = 32;
/**
* The sequence argument contains UIDs instead of sequence numbers
* @link http://php.net/manual/en/imap.constants.php
*/
const ST_UID = 1;
const ST_SILENT = 2;
const ST_MSGN = 3;
const ST_SET = 4;
/**
* the sequence numbers contain UIDS
* @link http://php.net/manual/en/imap.constants.php
*/
const CP_UID = 1;
/**
* Delete the messages from the current mailbox after copying
* with <b>imap_mail_copy</b>
* @link http://php.net/manual/en/imap.constants.php
*/
const CP_MOVE = 2;
/**
* Return UIDs instead of sequence numbers
* @link http://php.net/manual/en/imap.constants.php
*/
const SE_UID = 1;
const SE_FREE = 2;
/**
* Don't prefetch searched messages
* @link http://php.net/manual/en/imap.constants.php
*/
const SE_NOPREFETCH = 4;
const SO_FREE = 8;
const SO_NOSERVER = 16;
const SA_MESSAGES = 1;
const SA_RECENT = 2;
const SA_UNSEEN = 4;
const SA_UIDNEXT = 8;
const SA_UIDVALIDITY = 16;
const SA_ALL = 31;
/**
* This mailbox has no "children" (there are no
* mailboxes below this one).
* @link http://php.net/manual/en/imap.constants.php
*/
const LATT_NOINFERIORS = 1;
/**
* This is only a container, not a mailbox - you
* cannot open it.
* @link http://php.net/manual/en/imap.constants.php
*/
const LATT_NOSELECT = 2;
/**
* This mailbox is marked. Only used by UW-IMAPD.
* @link http://php.net/manual/en/imap.constants.php
*/
const LATT_MARKED = 4;
/**
* This mailbox is not marked. Only used by
* UW-IMAPD.
* @link http://php.net/manual/en/imap.constants.php
*/
const LATT_UNMARKED = 8;
const LATT_REFERRAL = 16;
const LATT_HASCHILDREN = 32;
const LATT_HASNOCHILDREN = 64;
/**
* Sort criteria for <b>imap_sort</b>:
* message Date
* @link http://php.net/manual/en/imap.constants.php
*/
const SORTDATE = 0;
/**
* Sort criteria for <b>imap_sort</b>:
* arrival date
* @link http://php.net/manual/en/imap.constants.php
*/
const SORTARRIVAL = 1;
/**
* Sort criteria for <b>imap_sort</b>:
* mailbox in first From address
* @link http://php.net/manual/en/imap.constants.php
*/
const SORTFROM = 2;
/**
* Sort criteria for <b>imap_sort</b>:
* message subject
* @link http://php.net/manual/en/imap.constants.php
*/
const SORTSUBJECT = 3;
/**
* Sort criteria for <b>imap_sort</b>:
* mailbox in first To address
* @link http://php.net/manual/en/imap.constants.php
*/
const SORTTO = 4;
/**
* Sort criteria for <b>imap_sort</b>:
* mailbox in first cc address
* @link http://php.net/manual/en/imap.constants.php
*/
const SORTCC = 5;
/**
* Sort criteria for <b>imap_sort</b>:
* size of message in octets
* @link http://php.net/manual/en/imap.constants.php
*/
const SORTSIZE = 6;
const TYPETEXT = 0;
const TYPEMULTIPART = 1;
const TYPEMESSAGE = 2;
const TYPEAPPLICATION = 3;
const TYPEAUDIO = 4;
const TYPEIMAGE = 5;
const TYPEVIDEO = 6;
const TYPEMODEL = 7;
const TYPEOTHER = 8;
const ENC7BIT = 0;
const ENC8BIT = 1;
const ENCBINARY = 2;
const ENCBASE64 = 3;
const ENCQUOTEDPRINTABLE = 4;
const ENCOTHER = 5;
/**
* Garbage collector, clear message cache elements.
* @link http://php.net/manual/en/imap.constants.php
*/
const IMAP_GC_ELT = 1;
/**
* Garbage collector, clear envelopes and bodies.
* @link http://php.net/manual/en/imap.constants.php
*/
const IMAP_GC_ENV = 2;
/**
* Garbage collector, clear texts.
* @link http://php.net/manual/en/imap.constants.php
*/
const IMAP_GC_TEXTS = 4;
}

1739
plugins/php-imap/src/Message.php Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,324 @@
<?php
/*
* File: Part.php
* Category: -
* Author: M.Goldenbaum
* Created: 17.09.20 20:38
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP;
use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException;
/**
* Class Part
*
* @package Webklex\PHPIMAP
*/
class Part {
/**
* Raw part
*
* @var string $raw
*/
public string $raw = "";
/**
* Part type
*
* @var int $type
*/
public int $type = IMAP::MESSAGE_TYPE_TEXT;
/**
* Part content
*
* @var string $content
*/
public string $content = "";
/**
* Part subtype
*
* @var ?string $subtype
*/
public ?string $subtype = null;
/**
* Part charset - if available
*
* @var string $charset
*/
public string $charset = "utf-8";
/**
* Part encoding method
*
* @var int $encoding
*/
public int $encoding = IMAP::MESSAGE_ENC_OTHER;
/**
* Alias to check if the part is an attachment
*
* @var boolean $ifdisposition
*/
public bool $ifdisposition = false;
/**
* Indicates if the part is an attachment
*
* @var ?string $disposition
*/
public ?string $disposition = null;
/**
* Alias to check if the part has a description
*
* @var boolean $ifdescription
*/
public bool $ifdescription = false;
/**
* Part description if available
*
* @var ?string $description
*/
public ?string $description = null;
/**
* Part filename if available
*
* @var ?string $filename
*/
public ?string $filename = null;
/**
* Part name if available
*
* @var ?string $name
*/
public ?string $name = null;
/**
* Part id if available
*
* @var ?string $id
*/
public ?string $id = null;
/**
* The part number of the current part
*
* @var integer $part_number
*/
public int $part_number = 0;
/**
* Part length in bytes
*
* @var integer $bytes
*/
public int $bytes;
/**
* Part content type
*
* @var string|null $content_type
*/
public ?string $content_type = null;
/**
* @var ?Header $header
*/
private ?Header $header;
/**
* @var Config $config
*/
protected Config $config;
/**
* Part constructor.
* @param string $raw_part
* @param Config $config
* @param Header|null $header
* @param integer $part_number
*
* @throws InvalidMessageDateException
*/
public function __construct(string $raw_part, Config $config, Header $header = null, int $part_number = 0) {
$this->raw = $raw_part;
$this->config = $config;
$this->header = $header;
$this->part_number = $part_number;
$this->parse();
}
/**
* Parse the raw parts
*
* @throws InvalidMessageDateException
*/
protected function parse(): void {
if ($this->header === null) {
$body = $this->findHeaders();
}else{
$body = $this->raw;
}
$this->parseDisposition();
$this->parseDescription();
$this->parseEncoding();
$this->charset = $this->header->get("charset")->first();
$this->name = $this->header->get("name");
$this->filename = $this->header->get("filename");
if($this->header->get("id")->exist()) {
$this->id = $this->header->get("id");
} else if($this->header->get("x_attachment_id")->exist()){
$this->id = $this->header->get("x_attachment_id");
} else if($this->header->get("content_id")->exist()){
$this->id = strtr($this->header->get("content_id"), [
'<' => '',
'>' => ''
]);
}
$content_types = $this->header->get("content_type")->all();
if(!empty($content_types)){
$this->subtype = $this->parseSubtype($content_types);
$content_type = $content_types[0];
$parts = explode(';', $content_type);
$this->content_type = trim($parts[0]);
}
$this->content = trim(rtrim($body));
$this->bytes = strlen($this->content);
}
/**
* Find all available headers and return the leftover body segment
*
* @return string
* @throws InvalidMessageDateException
*/
private function findHeaders(): string {
$body = $this->raw;
while (($pos = strpos($body, "\r\n")) > 0) {
$body = substr($body, $pos + 2);
}
$headers = substr($this->raw, 0, strlen($body) * -1);
$body = substr($body, 0, -2);
$this->header = new Header($headers, $this->config);
return $body;
}
/**
* Try to parse the subtype if any is present
* @param $content_type
*
* @return ?string
*/
private function parseSubtype($content_type): ?string {
if (is_array($content_type)) {
foreach ($content_type as $part){
if ((strpos($part, "/")) !== false){
return $this->parseSubtype($part);
}
}
return null;
}
if (($pos = strpos($content_type, "/")) !== false){
return substr(explode(";", $content_type)[0], $pos + 1);
}
return null;
}
/**
* Try to parse the disposition if any is present
*/
private function parseDisposition(): void {
$content_disposition = $this->header->get("content_disposition")->first();
if($content_disposition) {
$this->ifdisposition = true;
$this->disposition = (is_array($content_disposition)) ? implode(' ', $content_disposition) : explode(";", $content_disposition)[0];
}
}
/**
* Try to parse the description if any is present
*/
private function parseDescription(): void {
$content_description = $this->header->get("content_description")->first();
if($content_description) {
$this->ifdescription = true;
$this->description = $content_description;
}
}
/**
* Try to parse the encoding if any is present
*/
private function parseEncoding(): void {
$encoding = $this->header->get("content_transfer_encoding")->first();
if($encoding) {
$this->encoding = match (strtolower($encoding)) {
"quoted-printable" => IMAP::MESSAGE_ENC_QUOTED_PRINTABLE,
"base64" => IMAP::MESSAGE_ENC_BASE64,
"7bit" => IMAP::MESSAGE_ENC_7BIT,
"8bit" => IMAP::MESSAGE_ENC_8BIT,
"binary" => IMAP::MESSAGE_ENC_BINARY,
default => IMAP::MESSAGE_ENC_OTHER,
};
}
}
/**
* Check if the current part represents an attachment
*
* @return bool
*/
public function isAttachment(): bool {
$valid_disposition = in_array(strtolower($this->disposition ?? ''), $this->config->get('options.dispositions'));
if ($this->type == IMAP::MESSAGE_TYPE_TEXT && ($this->ifdisposition == 0 || empty($this->disposition) || !$valid_disposition)) {
if (($this->subtype == null || in_array((strtolower($this->subtype)), ["plain", "html"])) && $this->filename == null && $this->name == null) {
return false;
}
}
if ($this->disposition === "inline" && $this->filename == null && $this->name == null && !$this->header->has("content_id")) {
return false;
}
return true;
}
/**
* Get the part header
*
* @return Header|null
*/
public function getHeader(): ?Header {
return $this->header;
}
/**
* Get the Config instance
*
* @return Config
*/
public function getConfig(): Config {
return $this->config;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,555 @@
<?php
/*
* File: Query.php
* Category: -
* Author: M. Goldenbaum
* Created: 21.07.18 18:54
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Query;
use Closure;
use Illuminate\Support\Str;
use Webklex\PHPIMAP\Exceptions\InvalidWhereQueryCriteriaException;
use Webklex\PHPIMAP\Exceptions\MethodNotFoundException;
use Webklex\PHPIMAP\Exceptions\MessageSearchValidationException;
/**
* Class WhereQuery
*
* @package Webklex\PHPIMAP\Query
*
* @method WhereQuery all()
* @method WhereQuery answered()
* @method WhereQuery deleted()
* @method WhereQuery new()
* @method WhereQuery old()
* @method WhereQuery recent()
* @method WhereQuery seen()
* @method WhereQuery unanswered()
* @method WhereQuery undeleted()
* @method WhereQuery unflagged()
* @method WhereQuery unseen()
* @method WhereQuery not()
* @method WhereQuery unkeyword($value)
* @method WhereQuery to($value)
* @method WhereQuery text($value)
* @method WhereQuery subject($value)
* @method WhereQuery since($date)
* @method WhereQuery on($date)
* @method WhereQuery keyword($value)
* @method WhereQuery from($value)
* @method WhereQuery flagged()
* @method WhereQuery cc($value)
* @method WhereQuery body($value)
* @method WhereQuery before($date)
* @method WhereQuery bcc($value)
* @method WhereQuery inReplyTo($value)
* @method WhereQuery messageId($value)
*
* @mixin Query
*/
class WhereQuery extends Query {
/**
* @var array $available_criteria
*/
protected array $available_criteria = [
'OR', 'AND',
'ALL', 'ANSWERED', 'BCC', 'BEFORE', 'BODY', 'CC', 'DELETED', 'FLAGGED', 'FROM', 'KEYWORD',
'NEW', 'NOT', 'OLD', 'ON', 'RECENT', 'SEEN', 'SINCE', 'SUBJECT', 'TEXT', 'TO',
'UNANSWERED', 'UNDELETED', 'UNFLAGGED', 'UNKEYWORD', 'UNSEEN', 'UID'
];
/**
* Magic method in order to allow alias usage of all "where" methods in an optional connection with "NOT"
* @param string $name
* @param array|null $arguments
*
* @return mixed
* @throws InvalidWhereQueryCriteriaException
* @throws MethodNotFoundException
*/
public function __call(string $name, ?array $arguments) {
$that = $this;
$name = Str::camel($name);
if (strtolower(substr($name, 0, 3)) === 'not') {
$that = $that->whereNot();
$name = substr($name, 3);
}
if (!str_contains(strtolower($name), "where")) {
$method = 'where' . ucfirst($name);
} else {
$method = lcfirst($name);
}
if (method_exists($this, $method) === true) {
return call_user_func_array([$that, $method], $arguments);
}
throw new MethodNotFoundException("Method " . self::class . '::' . $method . '() is not supported');
}
/**
* Validate a given criteria
* @param $criteria
*
* @return string
* @throws InvalidWhereQueryCriteriaException
*/
protected function validate_criteria($criteria): string {
$command = strtoupper($criteria);
if (str_starts_with($command, "CUSTOM ")) {
return substr($criteria, 7);
}
if (in_array($command, $this->available_criteria) === false) {
throw new InvalidWhereQueryCriteriaException("Invalid imap search criteria: $command");
}
return $criteria;
}
/**
* Register search parameters
* @param mixed $criteria
* @param mixed $value
*
* @return $this
* @throws InvalidWhereQueryCriteriaException
*
* Examples:
* $query->from("someone@email.tld")->seen();
* $query->whereFrom("someone@email.tld")->whereSeen();
* $query->where([["FROM" => "someone@email.tld"], ["SEEN"]]);
* $query->where(["FROM" => "someone@email.tld"])->where(["SEEN"]);
* $query->where(["FROM" => "someone@email.tld", "SEEN"]);
* $query->where("FROM", "someone@email.tld")->where("SEEN");
*/
public function where(mixed $criteria, mixed $value = null): static {
if (is_array($criteria)) {
foreach ($criteria as $key => $value) {
if (is_numeric($key)) {
$this->where($value);
}else{
$this->where($key, $value);
}
}
} else {
$this->push_search_criteria($criteria, $value);
}
return $this;
}
/**
* Push a given search criteria and value pair to the search query
* @param $criteria string
* @param $value mixed
*
* @throws InvalidWhereQueryCriteriaException
*/
protected function push_search_criteria(string $criteria, mixed $value): void {
$criteria = $this->validate_criteria($criteria);
$value = $this->parse_value($value);
if ($value === '') {
$this->query->push([$criteria]);
} else {
$this->query->push([$criteria, $value]);
}
}
/**
* @param Closure|null $closure
*
* @return $this
*/
public function orWhere(Closure $closure = null): static {
$this->query->push(['OR']);
if ($closure !== null) $closure($this);
return $this;
}
/**
* @param Closure|null $closure
*
* @return $this
*/
public function andWhere(Closure $closure = null): static {
$this->query->push(['AND']);
if ($closure !== null) $closure($this);
return $this;
}
/**
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereAll(): static {
return $this->where('ALL');
}
/**
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereAnswered(): static {
return $this->where('ANSWERED');
}
/**
* @param string $value
*
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereBcc(string $value): static {
return $this->where('BCC', $value);
}
/**
* @param mixed $value
* @return $this
* @throws InvalidWhereQueryCriteriaException
* @throws MessageSearchValidationException
*/
public function whereBefore(mixed $value): static {
$date = $this->parse_date($value);
return $this->where('BEFORE', $date);
}
/**
* @param string $value
*
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereBody(string $value): static {
return $this->where('BODY', $value);
}
/**
* @param string $value
*
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereCc(string $value): static {
return $this->where('CC', $value);
}
/**
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereDeleted(): static {
return $this->where('DELETED');
}
/**
* @param string $value
*
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereFlagged(string $value): static {
return $this->where('FLAGGED', $value);
}
/**
* @param string $value
*
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereFrom(string $value): static {
return $this->where('FROM', $value);
}
/**
* @param string $value
*
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereKeyword(string $value): static {
return $this->where('KEYWORD', $value);
}
/**
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereNew(): static {
return $this->where('NEW');
}
/**
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereNot(): static {
return $this->where('NOT');
}
/**
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereOld(): static {
return $this->where('OLD');
}
/**
* @param mixed $value
*
* @return $this
* @throws MessageSearchValidationException
* @throws InvalidWhereQueryCriteriaException
*/
public function whereOn(mixed $value): static {
$date = $this->parse_date($value);
return $this->where('ON', $date);
}
/**
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereRecent(): static {
return $this->where('RECENT');
}
/**
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereSeen(): static {
return $this->where('SEEN');
}
/**
* @param mixed $value
*
* @return $this
* @throws MessageSearchValidationException
* @throws InvalidWhereQueryCriteriaException
*/
public function whereSince(mixed $value): static {
$date = $this->parse_date($value);
return $this->where('SINCE', $date);
}
/**
* @param string $value
*
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereSubject(string $value): static {
return $this->where('SUBJECT', $value);
}
/**
* @param string $value
*
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereText(string $value): static {
return $this->where('TEXT', $value);
}
/**
* @param string $value
*
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereTo(string $value): static {
return $this->where('TO', $value);
}
/**
* @param string $value
*
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereUnkeyword(string $value): static {
return $this->where('UNKEYWORD', $value);
}
/**
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereUnanswered(): static {
return $this->where('UNANSWERED');
}
/**
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereUndeleted(): static {
return $this->where('UNDELETED');
}
/**
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereUnflagged(): static {
return $this->where('UNFLAGGED');
}
/**
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereUnseen(): static {
return $this->where('UNSEEN');
}
/**
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereNoXSpam(): static {
return $this->where("CUSTOM X-Spam-Flag NO");
}
/**
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereIsXSpam(): static {
return $this->where("CUSTOM X-Spam-Flag YES");
}
/**
* Search for a specific header value
* @param $header
* @param $value
*
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereHeader($header, $value): static {
return $this->where("CUSTOM HEADER $header $value");
}
/**
* Search for a specific message id
* @param $messageId
*
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereMessageId($messageId): static {
return $this->whereHeader("Message-ID", $messageId);
}
/**
* Search for a specific message id
* @param $messageId
*
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereInReplyTo($messageId): static {
return $this->whereHeader("In-Reply-To", $messageId);
}
/**
* @param $country_code
*
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereLanguage($country_code): static {
return $this->where("Content-Language $country_code");
}
/**
* Get message be it UID.
*
* @param int|string $uid
*
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereUid(int|string $uid): static {
return $this->where('UID', $uid);
}
/**
* Get messages by their UIDs.
*
* @param array<int, int> $uids
*
* @return $this
* @throws InvalidWhereQueryCriteriaException
*/
public function whereUidIn(array $uids): static {
$uids = implode(',', $uids);
return $this->where('UID', $uids);
}
/**
* Apply the callback if the given "value" is truthy.
* copied from @url https://github.com/laravel/framework/blob/8.x/src/Illuminate/Support/Traits/Conditionable.php
*
* @param mixed $value
* @param callable $callback
* @param callable|null $default
* @return $this|null
*/
public function when(mixed $value, callable $callback, ?callable $default = null): mixed {
if ($value) {
return $callback($this, $value) ?: $this;
} elseif ($default) {
return $default($this, $value) ?: $this;
}
return $this;
}
/**
* Apply the callback if the given "value" is falsy.
* copied from @url https://github.com/laravel/framework/blob/8.x/src/Illuminate/Support/Traits/Conditionable.php
*
* @param mixed $value
* @param callable $callback
* @param callable|null $default
* @return $this|mixed
*/
public function unless(mixed $value, callable $callback, ?callable $default = null): mixed {
if (!$value) {
return $callback($this, $value) ?: $this;
} elseif ($default) {
return $default($this, $value) ?: $this;
}
return $this;
}
/**
* Get all available search criteria
*
* @return array|string[]
*/
public function getAvailableCriteria(): array {
return $this->available_criteria;
}
}

View File

@@ -0,0 +1,169 @@
<?php
/*
* File: Structure.php
* Category: -
* Author: M.Goldenbaum
* Created: 17.09.20 20:38
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP;
use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException;
use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException;
/**
* Class Structure
*
* @package Webklex\PHPIMAP
*/
class Structure {
/**
* Raw structure
*
* @var string $raw
*/
public string $raw = "";
/**
* @var Header $header
*/
private Header $header;
/**
* Message type (if multipart or not)
*
* @var int $type
*/
public int $type = IMAP::MESSAGE_TYPE_TEXT;
/**
* All available parts
*
* @var Part[] $parts
*/
public array $parts = [];
/**
* Options holder
*
* @var array $options
*/
protected array $options = [];
/**
* Structure constructor.
* @param $raw_structure
* @param Header $header
*
* @throws MessageContentFetchingException
* @throws InvalidMessageDateException
*/
public function __construct($raw_structure, Header $header) {
$this->raw = $raw_structure;
$this->header = $header;
$this->options = $header->getConfig()->get('options');
$this->parse();
}
/**
* Parse the given raw structure
*
* @throws MessageContentFetchingException
* @throws InvalidMessageDateException
*/
protected function parse(): void {
$this->findContentType();
$this->parts = $this->find_parts();
}
/**
* Determine the message content type
*/
public function findContentType(): void {
$content_type = $this->header->get("content_type")->first();
if($content_type && stripos($content_type, 'multipart') === 0) {
$this->type = IMAP::MESSAGE_TYPE_MULTIPART;
}else{
$this->type = IMAP::MESSAGE_TYPE_TEXT;
}
}
/**
* Find all available headers and return the leftover body segment
* @var string $context
* @var integer $part_number
*
* @return Part[]
* @throws InvalidMessageDateException
*/
private function parsePart(string $context, int $part_number = 0): array {
$body = $context;
while (($pos = strpos($body, "\r\n")) > 0) {
$body = substr($body, $pos + 2);
}
$headers = substr($context, 0, strlen($body) * -1);
$body = substr($body, 0, -2);
$config = $this->header->getConfig();
$headers = new Header($headers, $config);
if (($boundary = $headers->getBoundary()) !== null) {
$parts = $this->detectParts($boundary, $body, $part_number);
if(count($parts) > 1) {
return $parts;
}
}
return [new Part($body, $this->header->getConfig(), $headers, $part_number)];
}
/**
* @param string $boundary
* @param string $context
* @param int $part_number
*
* @return array
* @throws InvalidMessageDateException
*/
private function detectParts(string $boundary, string $context, int $part_number = 0): array {
$base_parts = explode( $boundary, $context);
$final_parts = [];
foreach($base_parts as $ctx) {
$ctx = substr($ctx, 2);
if ($ctx !== "--" && $ctx != "" && $ctx != "\r\n") {
$parts = $this->parsePart($ctx, $part_number);
foreach ($parts as $part) {
$final_parts[] = $part;
$part_number = $part->part_number;
}
$part_number++;
}
}
return $final_parts;
}
/**
* Find all available parts
*
* @return array
* @throws MessageContentFetchingException
* @throws InvalidMessageDateException
*/
public function find_parts(): array {
if($this->type === IMAP::MESSAGE_TYPE_MULTIPART) {
if (($boundary = $this->header->getBoundary()) === null) {
throw new MessageContentFetchingException("no content found", 0);
}
return $this->detectParts($boundary, $this->raw);
}
return [new Part($this->raw, $this->header->getConfig(), $this->header)];
}
}

View File

@@ -0,0 +1,26 @@
<?php
/*
* File: AttachmentCollection.php
* Category: Collection
* Author: M. Goldenbaum
* Created: 16.03.18 03:13
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Support;
use Illuminate\Support\Collection;
use Webklex\PHPIMAP\Attachment;
/**
* Class AttachmentCollection
*
* @package Webklex\PHPIMAP\Support
* @implements Collection<int, Attachment>
*/
class AttachmentCollection extends PaginatedCollection {
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* File: FlagCollection.php
* Category: Collection
* Author: M. Goldenbaum
* Created: 21.07.18 23:10
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Support;
use Illuminate\Support\Collection;
/**
* Class FlagCollection
*
* @package Webklex\PHPIMAP\Support
* @implements Collection<string, string>
*/
class FlagCollection extends PaginatedCollection {
}

View File

@@ -0,0 +1,26 @@
<?php
/*
* File: FolderCollection.php
* Category: Collection
* Author: M. Goldenbaum
* Created: 18.03.18 02:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Support;
use Illuminate\Support\Collection;
use Webklex\PHPIMAP\Folder;
/**
* Class FolderCollection
*
* @package Webklex\PHPIMAP\Support
* @implements Collection<int, Folder>
*/
class FolderCollection extends PaginatedCollection {
}

View File

@@ -0,0 +1,45 @@
<?php
/*
* File: AttachmentMask.php
* Category: Mask
* Author: M.Goldenbaum
* Created: 14.03.19 20:49
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Support\Masks;
use Webklex\PHPIMAP\Attachment;
/**
* Class AttachmentMask
*
* @package Webklex\PHPIMAP\Support\Masks
* @mixin Attachment
*/
class AttachmentMask extends Mask {
/** @var Attachment $parent */
protected mixed $parent;
/**
* Get the attachment content base64 encoded
*
* @return string|null
*/
public function getContentBase64Encoded(): ?string {
return base64_encode($this->parent->content);
}
/**
* Get a base64 image src string
*
* @return string|null
*/
public function getImageSrc(): ?string {
return 'data:'.$this->parent->content_type.';base64,'.$this->getContentBase64Encoded();
}
}

View File

@@ -0,0 +1,137 @@
<?php
/*
* File: Mask.php
* Category: Mask
* Author: M.Goldenbaum
* Created: 14.03.19 20:49
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Support\Masks;
use Illuminate\Support\Str;
use Webklex\PHPIMAP\Exceptions\MethodNotFoundException;
/**
* Class Mask
*
* @package Webklex\PHPIMAP\Support\Masks
*/
class Mask {
/**
* Available attributes
*
* @var array $attributes
*/
protected array $attributes = [];
/**
* Parent instance
*
* @var mixed $parent
*/
protected mixed $parent;
/**
* Mask constructor.
* @param $parent
*/
public function __construct($parent) {
$this->parent = $parent;
if(method_exists($this->parent, 'getAttributes')){
$this->attributes = array_merge($this->attributes, $this->parent->getAttributes());
}
$this->boot();
}
/**
* Boot method made to be used by any custom mask
*/
protected function boot(): void {}
/**
* Call dynamic attribute setter and getter methods and inherit the parent calls
* @param string $method
* @param array $arguments
*
* @return mixed
* @throws MethodNotFoundException
*/
public function __call(string $method, array $arguments) {
if(strtolower(substr($method, 0, 3)) === 'get') {
$name = Str::snake(substr($method, 3));
if(isset($this->attributes[$name])) {
return $this->attributes[$name];
}
}elseif (strtolower(substr($method, 0, 3)) === 'set') {
$name = Str::snake(substr($method, 3));
if(isset($this->attributes[$name])) {
$this->attributes[$name] = array_pop($arguments);
return $this->attributes[$name];
}
}
if(method_exists($this->parent, $method) === true){
return call_user_func_array([$this->parent, $method], $arguments);
}
throw new MethodNotFoundException("Method ".self::class.'::'.$method.'() is not supported');
}
/**
* Magic setter
* @param $name
* @param $value
*
* @return mixed
*/
public function __set($name, $value) {
$this->attributes[$name] = $value;
return $this->attributes[$name];
}
/**
* Magic getter
* @param $name
*
* @return mixed|null
*/
public function __get($name) {
if(isset($this->attributes[$name])) {
return $this->attributes[$name];
}
return null;
}
/**
* Get the parent instance
*
* @return mixed
*/
public function getParent(): mixed {
return $this->parent;
}
/**
* Get all available attributes
*
* @return array
*/
public function getAttributes(): array {
return $this->attributes;
}
}

View File

@@ -0,0 +1,87 @@
<?php
/*
* File: MessageMask.php
* Category: Mask
* Author: M.Goldenbaum
* Created: 14.03.19 20:49
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Support\Masks;
use Webklex\PHPIMAP\Attachment;
use Webklex\PHPIMAP\Message;
/**
* Class MessageMask
*
* @package Webklex\PHPIMAP\Support\Masks
* @mixin Message
*/
class MessageMask extends Mask {
/** @var Message $parent */
protected mixed $parent;
/**
* Get the message html body
*
* @return null
*/
public function getHtmlBody(){
$bodies = $this->parent->getBodies();
if (!isset($bodies['html'])) {
return null;
}
if(is_object($bodies['html']) && property_exists($bodies['html'], 'content')) {
return $bodies['html']->content;
}
return $bodies['html'];
}
/**
* Get the Message html body filtered by an optional callback
* @param callable|null $callback
*
* @return string|null
*/
public function getCustomHTMLBody(?callable $callback = null): ?string {
$body = $this->getHtmlBody();
if($body === null) return null;
if ($callback !== null) {
$aAttachment = $this->parent->getAttachments();
$aAttachment->each(function($oAttachment) use(&$body, $callback) {
/** @var Attachment $oAttachment */
if(is_callable($callback)) {
$body = $callback($body, $oAttachment);
}elseif(is_string($callback)) {
call_user_func($callback, [$body, $oAttachment]);
}
});
}
return $body;
}
/**
* Get the Message html body with embedded base64 images
* the resulting $body.
*
* @return string|null
*/
public function getHTMLBodyWithEmbeddedBase64Images(): ?string {
return $this->getCustomHTMLBody(function($body, $oAttachment){
/** @var Attachment $oAttachment */
if ($oAttachment->id) {
$body = str_replace('cid:'.$oAttachment->id, 'data:'.$oAttachment->getContentType().';base64, '.base64_encode($oAttachment->getContent()), $body);
}
return $body;
});
}
}

View File

@@ -0,0 +1,26 @@
<?php
/*
* File: MessageCollection.php
* Category: Collection
* Author: M. Goldenbaum
* Created: 16.03.18 03:13
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Support;
use Illuminate\Support\Collection;
use Webklex\PHPIMAP\Message;
/**
* Class MessageCollection
*
* @package Webklex\PHPIMAP\Support
* @implements Collection<int, Message>
*/
class MessageCollection extends PaginatedCollection {
}

View File

@@ -0,0 +1,82 @@
<?php
/*
* File: PaginatedCollection.php
* Category: Collection
* Author: M. Goldenbaum
* Created: 16.03.18 03:13
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Support;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Pagination\Paginator;
/**
* Class PaginatedCollection
*
* @package Webklex\PHPIMAP\Support
*/
class PaginatedCollection extends Collection {
/**
* Number of total entries
*
* @var int $total
*/
protected int $total = 0;
/**
* Paginate the current collection.
* @param int $per_page
* @param int|null $page
* @param string $page_name
* @param boolean $prepaginated
*
* @return LengthAwarePaginator
*/
public function paginate(int $per_page = 15, ?int $page = null, string $page_name = 'page', bool $prepaginated = false): LengthAwarePaginator {
$page = $page ?: Paginator::resolveCurrentPage($page_name);
$total = $this->total ?: $this->count();
$results = !$prepaginated && $total ? $this->forPage($page, $per_page)->toArray() : $this->all();
return $this->paginator($results, $total, $per_page, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => $page_name,
]);
}
/**
* Create a new length-aware paginator instance.
* @param array $items
* @param int $total
* @param int $per_page
* @param int|null $current_page
* @param array $options
*
* @return LengthAwarePaginator
*/
protected function paginator(array $items, int $total, int $per_page, ?int $current_page, array $options): LengthAwarePaginator {
return new LengthAwarePaginator($items, $total, $per_page, $current_page, $options);
}
/**
* Get and set the total amount
* @param null $total
*
* @return int|null
*/
public function total($total = null): ?int {
if($total === null) {
return $this->total;
}
return $this->total = $total;
}
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* File: HasEvents.php
* Category: -
* Author: M.Goldenbaum
* Created: 21.09.20 22:46
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP\Traits;
use Webklex\PHPIMAP\Events\Event;
use Webklex\PHPIMAP\Exceptions\EventNotFoundException;
/**
* Trait HasEvents
*
* @package Webklex\PHPIMAP\Traits
*/
trait HasEvents {
/**
* Event holder
*
* @var array $events
*/
protected array $events = [];
/**
* Set a specific event
* @param string $section
* @param string $event
* @param mixed $class
*/
public function setEvent(string $section, string $event, mixed $class): void {
if (isset($this->events[$section])) {
$this->events[$section][$event] = $class;
}
}
/**
* Set all events
* @param array $events
*/
public function setEvents(array $events): void {
$this->events = $events;
}
/**
* Get a specific event callback
* @param string $section
* @param string $event
*
* @return Event|string
* @throws EventNotFoundException
*/
public function getEvent(string $section, string $event): Event|string {
if (isset($this->events[$section])) {
return $this->events[$section][$event];
}
throw new EventNotFoundException();
}
/**
* Get all events
*
* @return array
*/
public function getEvents(): array {
return $this->events;
}
}

View File

@@ -0,0 +1,226 @@
<?php
/*
* File: imap.php
* Category: config
* Author: M. Goldenbaum
* Created: 24.09.16 22:36
* Updated: -
*
* Description:
* -
*/
return [
/*
|--------------------------------------------------------------------------
| Default date format
|--------------------------------------------------------------------------
|
| The default date format is used to convert any given Carbon::class object into a valid date string.
| These are currently known working formats: "d-M-Y", "d-M-y", "d M y"
|
*/
'date_format' => 'd-M-Y',
/*
|--------------------------------------------------------------------------
| Default account
|--------------------------------------------------------------------------
|
| The default account identifier. It will be used as default for any missing account parameters.
| If however the default account is missing a parameter the package default will be used.
| Set to 'false' [boolean] to disable this functionality.
|
*/
'default' => 'default',
/*
|--------------------------------------------------------------------------
| Available accounts
|--------------------------------------------------------------------------
|
| Please list all IMAP accounts which you are planning to use within the
| array below.
|
*/
'accounts' => [
'default' => [// account identifier
'host' => 'localhost',
'port' => 993,
'protocol' => 'imap', //might also use imap, [pop3 or nntp (untested)]
'encryption' => 'ssl', // Supported: false, 'ssl', 'tls'
'validate_cert' => true,
'username' => 'root@example.com',
'password' => '',
'authentication' => null,
'proxy' => [
'socket' => null,
'request_fulluri' => false,
'username' => null,
'password' => null,
],
"timeout" => 30,
"extensions" => []
],
/*
'gmail' => [ // account identifier
'host' => 'imap.gmail.com',
'port' => 993,
'encryption' => 'ssl',
'validate_cert' => true,
'username' => 'example@gmail.com',
'password' => 'PASSWORD',
'authentication' => 'oauth',
],
'another' => [ // account identifier
'host' => '',
'port' => 993,
'encryption' => false,
'validate_cert' => true,
'username' => '',
'password' => '',
'authentication' => null,
]
*/
],
/*
|--------------------------------------------------------------------------
| Available IMAP options
|--------------------------------------------------------------------------
|
| Available php imap config parameters are listed below
| -Delimiter (optional):
| This option is only used when calling $oClient->
| You can use any supported char such as ".", "/", (...)
| -Fetch option:
| IMAP::FT_UID - Message marked as read by fetching the body message
| IMAP::FT_PEEK - Fetch the message without setting the "seen" flag
| -Fetch sequence id:
| IMAP::ST_UID - Fetch message components using the message uid
| IMAP::ST_MSGN - Fetch message components using the message number
| -Body download option
| Default TRUE
| -Flag download option
| Default TRUE
| -Soft fail
| Default FALSE - Set to TRUE if you want to ignore certain exception while fetching bulk messages
| -RFC822
| Default TRUE - Set to FALSE to prevent the usage of \imap_rfc822_parse_headers().
| See https://github.com/Webklex/php-imap/issues/115 for more information.
| -Debug enable to trace communication traffic
| -UID cache enable the UID cache
| -Fallback date is used if the given message date could not be parsed
| -Boundary regex used to detect message boundaries. If you are having problems with empty messages, missing
| attachments or anything like this. Be advised that it likes to break which causes new problems..
| -Message key identifier option
| You can choose between the following:
| 'id' - Use the MessageID as array key (default, might cause hickups with yahoo mail)
| 'number' - Use the message number as array key (isn't always unique and can cause some interesting behavior)
| 'list' - Use the message list number as array key (incrementing integer (does not always start at 0 or 1)
| 'uid' - Use the message uid as array key (isn't always unique and can cause some interesting behavior)
| -Fetch order
| 'asc' - Order all messages ascending (probably results in oldest first)
| 'desc' - Order all messages descending (probably results in newest first)
| -Disposition types potentially considered an attachment
| Default ['attachment', 'inline']
| -Common folders
| Default folder locations and paths assumed if none is provided
| -Open IMAP options:
| DISABLE_AUTHENTICATOR - Disable authentication properties.
| Use 'GSSAPI' if you encounter the following
| error: "Kerberos error: No credentials cache
| file found (try running kinit) (...)"
| or ['GSSAPI','PLAIN'] if you are using outlook mail
| -Decoder options (currently only the message subject and attachment name decoder can be set)
| 'utf-8' - Uses imap_utf8($string) to decode a string
| 'mimeheader' - Uses mb_decode_mimeheader($string) to decode a string
|
*/
'options' => [
'delimiter' => '/',
'fetch' => \Webklex\PHPIMAP\IMAP::FT_PEEK,
'sequence' => \Webklex\PHPIMAP\IMAP::ST_UID,
'fetch_body' => true,
'fetch_flags' => true,
'soft_fail' => false,
'rfc822' => true,
'debug' => false,
'uid_cache' => true,
// 'fallback_date' => "01.01.1970 00:00:00",
'boundary' => '/boundary=(.*?(?=;)|(.*))/i',
'message_key' => 'list',
'fetch_order' => 'asc',
'dispositions' => ['attachment', 'inline'],
'common_folders' => [
"root" => "INBOX",
"junk" => "INBOX/Junk",
"draft" => "INBOX/Drafts",
"sent" => "INBOX/Sent",
"trash" => "INBOX/Trash",
],
'decoder' => [
'message' => 'utf-8', // mimeheader
'attachment' => 'utf-8' // mimeheader
],
'open' => [
// 'DISABLE_AUTHENTICATOR' => 'GSSAPI'
]
],
/*
|--------------------------------------------------------------------------
| Available flags
|--------------------------------------------------------------------------
|
| List all available / supported flags. Set to null to accept all given flags.
*/
'flags' => ['recent', 'flagged', 'answered', 'deleted', 'seen', 'draft'],
/*
|--------------------------------------------------------------------------
| Available events
|--------------------------------------------------------------------------
|
*/
'events' => [
"message" => [
'new' => \Webklex\PHPIMAP\Events\MessageNewEvent::class,
'moved' => \Webklex\PHPIMAP\Events\MessageMovedEvent::class,
'copied' => \Webklex\PHPIMAP\Events\MessageCopiedEvent::class,
'deleted' => \Webklex\PHPIMAP\Events\MessageDeletedEvent::class,
'restored' => \Webklex\PHPIMAP\Events\MessageRestoredEvent::class,
],
"folder" => [
'new' => \Webklex\PHPIMAP\Events\FolderNewEvent::class,
'moved' => \Webklex\PHPIMAP\Events\FolderMovedEvent::class,
'deleted' => \Webklex\PHPIMAP\Events\FolderDeletedEvent::class,
],
"flag" => [
'new' => \Webklex\PHPIMAP\Events\FlagNewEvent::class,
'deleted' => \Webklex\PHPIMAP\Events\FlagDeletedEvent::class,
],
],
/*
|--------------------------------------------------------------------------
| Available masking options
|--------------------------------------------------------------------------
|
| By using your own custom masks you can implement your own methods for
| a better and faster access and less code to write.
|
| Checkout the two examples custom_attachment_mask and custom_message_mask
| for a quick start.
|
| The provided masks below are used as the default masks.
*/
'masks' => [
'message' => \Webklex\PHPIMAP\Support\Masks\MessageMask::class,
'attachment' => \Webklex\PHPIMAP\Support\Masks\AttachmentMask::class
]
];