Included WebKlex PHP-IMAP Library in plugins folder to allow for future use when we convert IMAP to allow OAUTH2

This commit is contained in:
johnnyq 2024-01-11 12:51:11 -05:00
parent 9520148d4d
commit 625a6cac6c
63 changed files with 13259 additions and 0 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();
}
}

408
plugins/php-imap/Attachment.php Executable file
View File

@ -0,0 +1,408 @@
<?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 $oMessage
*/
protected Message $oMessage;
/**
* Used config
*
* @var array $config
*/
protected array $config = [];
/** @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 $oMessage
* @param Part $part
*/
public function __construct(Message $oMessage, Part $part) {
$this->config = ClientManager::get('options');
$this->oMessage = $oMessage;
$this->part = $part;
$this->part_number = $part->part_number;
if ($this->oMessage->getClient()) {
$default_mask = $this->oMessage->getClient()?->getDefaultAttachmentMask();
if ($default_mask != null) {
$this->mask = $default_mask;
}
} else {
$default_mask = ClientManager::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->oMessage->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->config['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->oMessage;
}
/**
* 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 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);
}
}

950
plugins/php-imap/Client.php Executable file
View File

@ -0,0 +1,950 @@
<?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;
/**
* 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 array $config
*
* @throws MaskNotFoundException
*/
public function __construct(array $config = []) {
$this->setConfig($config);
$this->setMaskFromConfig($config);
$this->setEventsFromConfig($config);
}
/**
* Client destructor
*
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
*/
public function __destruct() {
$this->disconnect();
}
/**
* Clone the current Client instance
*
* @return Client
*/
public function clone(): Client {
$client = new self();
$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, $config, $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 array $config
*
* @return self
*/
public function setConfig(array $config): Client {
$default_account = ClientManager::get('default');
$default_config = ClientManager::get("accounts.$default_account");
foreach ($this->default_account_config as $key => $value) {
$this->setAccountConfig($key, $config, $default_config);
}
return $this;
}
/**
* Get the current config
*
* @return array
*/
public function getConfig(): array {
$config = [];
foreach($this->default_account_config as $key => $value) {
$config[$key] = $this->$key;
}
return $config;
}
/**
* Set a specific account config
* @param string $key
* @param array $config
* @param array $default_config
*/
private function setAccountConfig(string $key, array $config, array $default_config): void {
$value = $this->default_account_config[$key];
if(isset($config[$key])) {
$value = $config[$key];
}elseif(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
* @param $config
*/
protected function setEventsFromConfig($config): void {
$this->events = ClientManager::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
* @param $config
*
* @throws MaskNotFoundException
*/
protected function setMaskFromConfig($config): void {
if(isset($config['masks'])){
if(isset($config['masks']['message'])) {
if(class_exists($config['masks']['message'])) {
$this->default_message_mask = $config['masks']['message'];
}else{
throw new MaskNotFoundException("Unknown mask provided: ".$config['masks']['message']);
}
}else{
$default_mask = ClientManager::getMask("message");
if($default_mask != ""){
$this->default_message_mask = $default_mask;
}else{
throw new MaskNotFoundException("Unknown message mask provided");
}
}
if(isset($config['masks']['attachment'])) {
if(class_exists($config['masks']['attachment'])) {
$this->default_attachment_mask = $config['masks']['attachment'];
}else{
throw new MaskNotFoundException("Unknown mask provided: ". $config['masks']['attachment']);
}
}else{
$default_mask = ClientManager::getMask("attachment");
if($default_mask != ""){
$this->default_attachment_mask = $default_mask;
}else{
throw new MaskNotFoundException("Unknown attachment mask provided");
}
}
}else{
$default_mask = ClientManager::getMask("message");
if($default_mask != ""){
$this->default_message_mask = $default_mask;
}else{
throw new MaskNotFoundException("Unknown message mask provided");
}
$default_mask = ClientManager::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->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->validate_cert, $this->encryption);
if (str_starts_with($protocol, "legacy-")) {
$protocol = substr($protocol, 7);
}
$this->connection->setProtocol($protocol);
}
if (ClientManager::get('options.debug')) {
$this->connection->enableDebug();
}
if (!ClientManager::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) ? ClientManager::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,293 @@
<?php
/*
* File: ClientManager.php
* Category: -
* Author: M. Goldenbaum
* Created: 19.01.17 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP;
/**
* Class ClientManager
*
* @package Webklex\IMAP
*
* @mixin Client
*/
class ClientManager {
/**
* All library config
*
* @var array $config
*/
public static array $config = [];
/**
* @var array $accounts
*/
protected array $accounts = [];
/**
* ClientManager constructor.
* @param array|string $config
*/
public function __construct(array|string $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 {
return new Client($config);
}
/**
* Get a dotted config parameter
* @param string $key
* @param null $default
*
* @return mixed|null
*/
public static function get(string $key, $default = null): mixed {
$parts = explode('.', $key);
$value = null;
foreach ($parts as $part) {
if ($value === null) {
if (isset(self::$config[$part])) {
$value = self::$config[$part];
} else {
break;
}
} else {
if (isset($value[$part])) {
$value = $value[$part];
} else {
break;
}
}
}
return $value === null ? $default : $value;
}
/**
* Get the mask for a given section
* @param string $section section name such as "message" or "attachment"
*
* @return string|null
*/
public static function getMask(string $section): ?string {
$default_masks = ClientManager::get("masks");
if (isset($default_masks[$section])) {
if (class_exists($default_masks[$section])) {
return $default_masks[$section];
}
}
return null;
}
/**
* Resolve a account instance.
* @param string|null $name
*
* @return Client
* @throws Exceptions\MaskNotFoundException
*/
public function account(string $name = null): Client {
$name = $name ?: $this->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->getClientConfig($name);
return new Client($config);
}
/**
* Get the account configuration.
* @param string|null $name
*
* @return array
*/
protected function getClientConfig(?string $name): array {
if ($name === null || $name === 'null' || $name === "") {
return ['driver' => 'null'];
}
$account = self::$config["accounts"][$name] ?? [];
return is_array($account) ? $account : [];
}
/**
* Get the name of the default account.
*
* @return string
*/
public function getDefaultAccount(): string {
return self::$config['default'];
}
/**
* Set the name of the default account.
* @param string $name
*
* @return void
*/
public function setDefaultAccount(string $name): void {
self::$config['default'] = $name;
}
/**
* 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
*
* @return $this
*/
public function setConfig(array|string $config): ClientManager {
if (is_array($config) === false) {
$config = require $config;
}
$config_key = 'imap';
$path = __DIR__ . '/config/' . $config_key . '.php';
$vendor_config = require $path;
$config = $this->array_merge_recursive_distinct($vendor_config, $config);
if (is_array($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);
}
}
}
}
}
self::$config = $config;
return $this;
}
/**
* 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|mixed
*
* @link http://www.php.net/manual/en/function.array-merge-recursive.php#96201
* @author Mark Roduner <mark.roduner@gmail.com>
*/
private function array_merge_recursive_distinct(): mixed {
$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] = $this->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;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,814 @@
<?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\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 bool $cert_validation set to false to skip SSL certificate validation
* @param mixed $encryption Connection encryption method
*/
public function __construct(bool $cert_validation = true, mixed $encryption = false) {
$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) {
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,
ClientManager::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
* @throws AuthFailedException
* @throws RuntimeException
*/
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,
] : [];
});
}
/**
* 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 a message number for a 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,366 @@
<?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\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
*/
public $stream = false;
/**
* 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) {
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/meta data 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;
}
}

View File

@ -0,0 +1,447 @@
<?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;
/**
* 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 {
}

572
plugins/php-imap/Folder.php Executable file
View File

@ -0,0 +1,572 @@
<?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;
/**
* 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", "UTF-8");
}
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);
}
/**
* 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 = ClientManager::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(null);
}
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 = ClientManager::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
*
* @return array
* @throws ConnectionFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws AuthFailedException
* @throws ResponseException
*/
public function getStatus(): array {
return $this->examine();
}
/**
* @throws ConnectionFailedException
* @throws ImapBadRequestException
* @throws ImapServerErrorException
* @throws RuntimeException
* @throws AuthFailedException
* @throws ResponseException
*/
public function loadStatus(): Folder {
$this->status = $this->getStatus();
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 = ClientManager::get('options.delimiter', '/');
}
$this->delimiter = $delimiter;
}
}

808
plugins/php-imap/Header.php Normal file
View File

@ -0,0 +1,808 @@
<?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 array $config
*/
protected array $config = [];
/**
* Fallback Encoding
*
* @var string
*/
public string $fallback_encoding = 'UTF-8';
/**
* Header constructor.
* @param string $raw_header
*
* @throws InvalidMessageDateException
*/
public function __construct(string $raw_header) {
$this->raw = $raw_header;
$this->config = ClientManager::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->config["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->config["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->config['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->config["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]{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->config["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->config["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 setConfig(array $config): Header {
$this->config = $config;
return $this;
}
}

375
plugins/php-imap/IMAP.php Normal file
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;
}

1703
plugins/php-imap/Message.php Executable file

File diff suppressed because it is too large Load Diff

308
plugins/php-imap/Part.php Normal file
View File

@ -0,0 +1,308 @@
<?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;
/**
* Part constructor.
* @param $raw_part
* @param Header|null $header
* @param integer $part_number
*
* @throws InvalidMessageDateException
*/
public function __construct($raw_part, Header $header = null, int $part_number = 0) {
$this->raw = $raw_part;
$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);
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 ?? ''), ClientManager::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;
}
}

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): WhereQuery {
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){
$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): WhereQuery {
$this->query->push(['OR']);
if ($closure !== null) $closure($this);
return $this;
}
/**
* @param Closure|null $closure
*
* @return $this
*/
public function andWhere(Closure $closure = null): WhereQuery {
$this->query->push(['AND']);
if ($closure !== null) $closure($this);
return $this;
}
/**
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereAll(): WhereQuery {
return $this->where('ALL');
}
/**
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereAnswered(): WhereQuery {
return $this->where('ANSWERED');
}
/**
* @param string $value
*
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereBcc(string $value): WhereQuery {
return $this->where('BCC', $value);
}
/**
* @param mixed $value
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
* @throws MessageSearchValidationException
*/
public function whereBefore(mixed $value): WhereQuery {
$date = $this->parse_date($value);
return $this->where('BEFORE', $date);
}
/**
* @param string $value
*
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereBody(string $value): WhereQuery {
return $this->where('BODY', $value);
}
/**
* @param string $value
*
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereCc(string $value): WhereQuery {
return $this->where('CC', $value);
}
/**
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereDeleted(): WhereQuery {
return $this->where('DELETED');
}
/**
* @param string $value
*
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereFlagged(string $value): WhereQuery {
return $this->where('FLAGGED', $value);
}
/**
* @param string $value
*
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereFrom(string $value): WhereQuery {
return $this->where('FROM', $value);
}
/**
* @param string $value
*
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereKeyword(string $value): WhereQuery {
return $this->where('KEYWORD', $value);
}
/**
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereNew(): WhereQuery {
return $this->where('NEW');
}
/**
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereNot(): WhereQuery {
return $this->where('NOT');
}
/**
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereOld(): WhereQuery {
return $this->where('OLD');
}
/**
* @param mixed $value
*
* @return WhereQuery
* @throws MessageSearchValidationException
* @throws InvalidWhereQueryCriteriaException
*/
public function whereOn(mixed $value): WhereQuery {
$date = $this->parse_date($value);
return $this->where('ON', $date);
}
/**
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereRecent(): WhereQuery {
return $this->where('RECENT');
}
/**
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereSeen(): WhereQuery {
return $this->where('SEEN');
}
/**
* @param mixed $value
*
* @return WhereQuery
* @throws MessageSearchValidationException
* @throws InvalidWhereQueryCriteriaException
*/
public function whereSince(mixed $value): WhereQuery {
$date = $this->parse_date($value);
return $this->where('SINCE', $date);
}
/**
* @param string $value
*
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereSubject(string $value): WhereQuery {
return $this->where('SUBJECT', $value);
}
/**
* @param string $value
*
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereText(string $value): WhereQuery {
return $this->where('TEXT', $value);
}
/**
* @param string $value
*
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereTo(string $value): WhereQuery {
return $this->where('TO', $value);
}
/**
* @param string $value
*
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereUnkeyword(string $value): WhereQuery {
return $this->where('UNKEYWORD', $value);
}
/**
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereUnanswered(): WhereQuery {
return $this->where('UNANSWERED');
}
/**
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereUndeleted(): WhereQuery {
return $this->where('UNDELETED');
}
/**
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereUnflagged(): WhereQuery {
return $this->where('UNFLAGGED');
}
/**
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereUnseen(): WhereQuery {
return $this->where('UNSEEN');
}
/**
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereNoXSpam(): WhereQuery {
return $this->where("CUSTOM X-Spam-Flag NO");
}
/**
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereIsXSpam(): WhereQuery {
return $this->where("CUSTOM X-Spam-Flag YES");
}
/**
* Search for a specific header value
* @param $header
* @param $value
*
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereHeader($header, $value): WhereQuery {
return $this->where("CUSTOM HEADER $header $value");
}
/**
* Search for a specific message id
* @param $messageId
*
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereMessageId($messageId): WhereQuery {
return $this->whereHeader("Message-ID", $messageId);
}
/**
* Search for a specific message id
* @param $messageId
*
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereInReplyTo($messageId): WhereQuery {
return $this->whereHeader("In-Reply-To", $messageId);
}
/**
* @param $country_code
*
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereLanguage($country_code): WhereQuery {
return $this->where("Content-Language $country_code");
}
/**
* Get message be it UID.
*
* @param int|string $uid
*
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereUid(int|string $uid): WhereQuery {
return $this->where('UID', $uid);
}
/**
* Get messages by their UIDs.
*
* @param array<int, int> $uids
*
* @return WhereQuery
* @throws InvalidWhereQueryCriteriaException
*/
public function whereUidIn(array $uids): WhereQuery {
$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,164 @@
<?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 = [];
/**
* Config holder
*
* @var array $config
*/
protected array $config = [];
/**
* 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->config = ClientManager::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);
$headers = new Header($headers);
if (($boundary = $headers->getBoundary()) !== null) {
return $this->detectParts($boundary, $body, $part_number);
}
return [new Part($body, $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)];
}
}

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,44 @@
<?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
*/
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,86 @@
<?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
*/
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;
}
}

1
plugins/php-imap/VERSION Normal file
View File

@ -0,0 +1 @@
5.5.0

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
]
];