1
0
mirror of https://github.com/freescout-helpdesk/freescout.git synced 2024-11-25 11:52:29 +01:00
freescout/overrides/Webklex/IMAP/Message.php
2018-11-12 09:12:57 +00:00

1146 lines
29 KiB
PHP

<?php
/*
* File: Message.php
* Category: -
* Author: M. Goldenbaum
* Created: 19.01.17 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\IMAP;
use Carbon\Carbon;
use Webklex\IMAP\Support\AttachmentCollection;
use Webklex\IMAP\Support\FlagCollection;
/**
* Class Message.
*/
class Message
{
/**
* Client instance.
*
* @var Client
*/
private $client = Client::class;
/**
* U ID.
*
* @var int
*/
public $uid = '';
/**
* Fetch body options.
*
* @var int
*/
public $fetch_options = null;
/**
* Fetch body options.
*
* @var bool
*/
public $fetch_body = null;
/**
* Fetch attachments options.
*
* @var bool
*/
public $fetch_attachment = null;
/**
* Fetch flags options.
*
* @var bool
*/
public $fetch_flags = null;
/**
* @var int
*/
public $msglist = 1;
/**
* @var int
*/
public $msgn = null;
/**
* @var string
*/
public $header = null;
/**
* @var null|object
*/
public $header_info = null;
/** @var null|string $raw_body */
public $raw_body = null;
/**
* Message header components.
*
* @var string
* @var mixed $message_no
* @var string $subject
* @var mixed $references
* @var mixed $date
* @var array $from
* @var array $to
* @var array $cc
* @var array $bcc
* @var array $reply_to
* @var string $in_reply_to
* @var array $sender
* @var array $flags
* @var array $priority
*/
public $message_id = '';
public $message_no = null;
public $subject = '';
public $references = null;
public $date = null;
public $from = [];
public $to = [];
public $cc = [];
public $bcc = [];
public $reply_to = [];
public $in_reply_to = '';
public $sender = [];
public $priority = 0;
/**
* Message body components.
*
* @var array
* @var AttachmentCollection|array $attachments
* @var FlagCollection|array $flags
*/
public $bodies = [];
public $attachments = [];
public $flags = [];
/**
* 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 TYPE_TEXT = 0;
const TYPE_MULTIPART = 1;
const ENC_7BIT = 0;
const ENC_8BIT = 1;
const ENC_BINARY = 2;
const ENC_BASE64 = 3;
const ENC_QUOTED_PRINTABLE = 4;
const ENC_OTHER = 5;
const PRIORITY_UNKNOWN = 0;
const PRIORITY_HIGHEST = 1;
const PRIORITY_HIGH = 2;
const PRIORITY_NORMAL = 3;
const PRIORITY_LOW = 4;
const PRIORITY_LOWEST = 5;
/**
* Message constructor.
*
* @param int $uid
* @param int|null $msglist
* @param Client $client
* @param int|null $fetch_options
* @param bool $fetch_body
* @param bool $fetch_attachment
* @param bool $fetch_flags
*
* @throws Exceptions\ConnectionFailedException
*/
public function __construct($uid, $msglist, Client $client, $fetch_options = null, $fetch_body = false, $fetch_attachment = false, $fetch_flags = false)
{
$this->setFetchOption($fetch_options);
$this->setFetchBodyOption($fetch_body);
$this->setFetchAttachmentOption($fetch_attachment);
$this->setFetchFlagsOption($fetch_flags);
$this->attachments = AttachmentCollection::make([]);
$this->flags = FlagCollection::make([]);
$this->msglist = $msglist;
$this->client = $client;
$this->uid = ($this->fetch_options == FT_UID) ? $uid : $uid;
$this->msgn = ($this->fetch_options == FT_UID) ? imap_msgno($this->client->getConnection(), $uid) : $uid;
$this->parseHeader();
if ($this->getFetchFlagsOption() === true) {
$this->parseFlags();
}
if ($this->getFetchBodyOption() === true) {
$this->parseBody();
}
}
/**
* Copy the current Messages to a mailbox.
*
* @param $mailbox
* @param int $options
*
* @throws Exceptions\ConnectionFailedException
*
* @return bool
*/
public function copy($mailbox, $options = 0)
{
return imap_mail_copy($this->client->getConnection(), $this->msglist, $mailbox, $options);
}
/**
* Move the current Messages to a mailbox.
*
* @param $mailbox
* @param int $options
*
* @throws Exceptions\ConnectionFailedException
*
* @return bool
*/
public function move($mailbox, $options = 0)
{
return imap_mail_move($this->client->getConnection(), $this->msglist, $mailbox, $options);
}
/**
* Check if the Message has a text body.
*
* @return bool
*/
public function hasTextBody()
{
return isset($this->bodies['text']);
}
/**
* Get the Message text body.
*
* @return mixed
*/
public function getTextBody()
{
if (!isset($this->bodies['text'])) {
return false;
}
return $this->bodies['text']->content;
}
/**
* Check if the Message has a html body.
*
* @return bool
*/
public function hasHTMLBody()
{
return isset($this->bodies['html']);
}
/**
* Get the Message html body.
*
* @var bool
*
* @return mixed
*/
public function getHTMLBody($replaceImages = false)
{
if (!isset($this->bodies['html'])) {
return false;
}
$body = $this->bodies['html']->content;
if ($replaceImages) {
$this->attachments->each(function ($oAttachment) use (&$body) {
if ($oAttachment->id && isset($oAttachment->img_src)) {
$body = str_replace('cid:'.$oAttachment->id, $oAttachment->img_src, $body);
}
});
}
return $body;
}
/**
* Parse all defined headers.
*
* @throws Exceptions\ConnectionFailedException
*
* @return void
*/
private function parseHeader()
{
$this->header = $header = imap_fetchheader($this->client->getConnection(), $this->uid, FT_UID);
if ($this->header) {
$header = imap_rfc822_parse_headers($this->header);
}
if (preg_match('/x\-priority\:.*([0-9]{1,2})/i', $this->header, $priority)) {
$priority = isset($priority[1]) ? (int) $priority[1] : 0;
switch ($priority) {
case self::PRIORITY_HIGHEST:
$this->priority = self::PRIORITY_HIGHEST;
break;
case self::PRIORITY_HIGH:
$this->priority = self::PRIORITY_HIGH;
break;
case self::PRIORITY_NORMAL:
$this->priority = self::PRIORITY_NORMAL;
break;
case self::PRIORITY_LOW:
$this->priority = self::PRIORITY_LOW;
break;
case self::PRIORITY_LOWEST:
$this->priority = self::PRIORITY_LOWEST;
break;
default:
$this->priority = self::PRIORITY_UNKNOWN;
break;
}
}
if (property_exists($header, 'subject')) {
$this->subject = mb_decode_mimeheader($header->subject);
}
if (property_exists($header, 'date')) {
$date = $header->date;
/*
* Exception handling for invalid dates
* Will be extended in the future
*
* Currently known invalid formats:
* ^ Datetime ^ Problem ^ Cause
* | Mon, 20 Nov 2017 20:31:31 +0800 (GMT+8:00) | Double timezone specification | A Windows feature
* | | 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 |
*
* Please report any new invalid timestamps to [#45](https://github.com/Webklex/laravel-imap/issues/45)
*/
try {
$this->date = Carbon::parse($date);
} catch (\Exception $e) {
switch (true) {
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;
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:
$date .= 'C';
break;
}
$this->date = Carbon::parse($date);
}
}
if (property_exists($header, 'from')) {
$this->from = $this->parseAddresses($header->from);
}
if (property_exists($header, 'to')) {
$this->to = $this->parseAddresses($header->to);
}
if (property_exists($header, 'cc')) {
$this->cc = $this->parseAddresses($header->cc);
}
if (property_exists($header, 'bcc')) {
$this->bcc = $this->parseAddresses($header->bcc);
}
if (property_exists($header, 'references')) {
$this->references = $header->references;
}
if (property_exists($header, 'reply_to')) {
$this->reply_to = $this->parseAddresses($header->reply_to);
}
if (property_exists($header, 'in_reply_to')) {
$this->in_reply_to = str_replace(['<', '>'], '', $header->in_reply_to);
}
if (property_exists($header, 'sender')) {
$this->sender = $this->parseAddresses($header->sender);
}
if (property_exists($header, 'message_id')) {
$this->message_id = str_replace(['<', '>'], '', $header->message_id);
}
if (property_exists($header, 'Msgno')) {
$messageNo = (int) trim($header->Msgno);
$this->message_no = ($this->fetch_options == FT_UID) ? $messageNo : imap_msgno($this->client->getConnection(), $messageNo);
} else {
$this->message_no = imap_msgno($this->client->getConnection(), $this->getUid());
}
}
/**
* Parse additional flags.
*
* @throws Exceptions\ConnectionFailedException
*
* @return void
*/
private function parseFlags()
{
$flags = imap_fetch_overview($this->client->getConnection(), $this->uid, FT_UID);
if (is_array($flags) && isset($flags[0])) {
if (property_exists($flags[0], 'recent')) {
$this->flags->put('recent', $flags[0]->recent);
}
if (property_exists($flags[0], 'flagged')) {
$this->flags->put('flagged', $flags[0]->flagged);
}
if (property_exists($flags[0], 'answered')) {
$this->flags->put('answered', $flags[0]->answered);
}
if (property_exists($flags[0], 'deleted')) {
$this->flags->put('deleted', $flags[0]->deleted);
}
if (property_exists($flags[0], 'seen')) {
$this->flags->put('seen', $flags[0]->seen);
}
if (property_exists($flags[0], 'draft')) {
$this->flags->put('draft', $flags[0]->draft);
}
}
}
/**
* Get the current Message header info.
*
* @throws Exceptions\ConnectionFailedException
*
* @return object
*/
public function getHeaderInfo()
{
if ($this->header_info == null) {
$this->header_info =
$this->header_info = imap_headerinfo($this->client->getConnection(), $this->getMessageNo());
}
return $this->header_info;
}
/**
* Parse Addresses.
*
* @param $list
*
* @return array
*/
private function parseAddresses($list)
{
$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;
}
// FreeScout fix
$personalParts = imap_mime_header_decode($address->personal);
$address->personal = '';
foreach ($personalParts as $p) {
$address->personal .= $p->text;
}
$address->mail = ($address->mailbox && $address->host) ? $address->mailbox.'@'.$address->host : false;
$address->full = ($address->personal) ? $address->personal.' <'.$address->mail.'>' : $address->mail;
$addresses[] = $address;
}
return $addresses;
}
/**
* Parse the Message body.
*
* @throws Exceptions\ConnectionFailedException
*
* @return $this
*/
public function parseBody()
{
$structure = imap_fetchstructure($this->client->getConnection(), $this->uid, FT_UID);
if (property_exists($structure, 'parts')) {
$parts = $structure->parts;
foreach ($parts as $part) {
foreach ($part->parameters as $parameter) {
if ($parameter->attribute == 'charset') {
$encoding = $parameter->value;
$parameter->value = preg_replace('/Content-Transfer-Encoding/', '', $encoding);
}
}
}
}
$this->fetchStructure($structure);
return $this;
}
/**
* Fetch the Message structure.
*
* @param $structure
* @param mixed $partNumber
*
* @throws Exceptions\ConnectionFailedException
*/
private function fetchStructure($structure, $partNumber = null)
{
if ($structure->type == self::TYPE_TEXT &&
($structure->ifdisposition == 0 ||
($structure->ifdisposition == 1 && !isset($structure->parts) && $partNumber == null)
)
) {
if ($structure->subtype == 'PLAIN') {
if (!$partNumber) {
$partNumber = 1;
}
$encoding = $this->getEncoding($structure);
$content = imap_fetchbody($this->client->getConnection(), $this->uid, $partNumber, $this->fetch_options | FT_UID);
$content = $this->decodeString($content, $structure->encoding);
$content = $this->convertEncoding($content, $encoding);
$body = new \stdClass();
$body->type = 'text';
$body->content = $content;
$this->bodies['text'] = $body;
$this->fetchAttachment($structure, $partNumber);
} elseif ($structure->subtype == 'HTML') {
if (!$partNumber) {
$partNumber = 1;
}
$encoding = $this->getEncoding($structure);
$content = imap_fetchbody($this->client->getConnection(), $this->uid, $partNumber, $this->fetch_options | FT_UID);
$content = $this->decodeString($content, $structure->encoding);
$content = $this->convertEncoding($content, $encoding);
$body = new \stdClass();
$body->type = 'html';
$body->content = $content;
$this->bodies['html'] = $body;
}
} elseif ($structure->type == self::TYPE_MULTIPART) {
foreach ($structure->parts as $index => $subStruct) {
$prefix = '';
if ($partNumber) {
$prefix = $partNumber.'.';
}
$this->fetchStructure($subStruct, $prefix.($index + 1));
}
} else {
if ($this->getFetchAttachmentOption() === true) {
$this->fetchAttachment($structure, $partNumber);
}
}
}
/**
* Fetch the Message attachment.
*
* @param object $structure
* @param mixed $partNumber
*
* @throws Exceptions\ConnectionFailedException
*/
protected function fetchAttachment($structure, $partNumber)
{
$oAttachment = new Attachment($this, $structure, $partNumber);
if ($oAttachment->getName() !== null) {
if ($oAttachment->getId() !== null) {
$this->attachments->put($oAttachment->getId(), $oAttachment);
} else {
$this->attachments->push($oAttachment);
}
}
}
/**
* Fail proof setter for $fetch_option.
*
* @param $option
*
* @return $this
*/
public function setFetchOption($option)
{
if (is_int($option) === true) {
$this->fetch_options = $option;
} elseif (is_null($option) === true) {
$config = config('imap.options.fetch', FT_UID);
$this->fetch_options = is_int($config) ? $config : 1;
}
return $this;
}
/**
* Fail proof setter for $fetch_body.
*
* @param $option
*
* @return $this
*/
public function setFetchBodyOption($option)
{
if (is_bool($option)) {
$this->fetch_body = $option;
} elseif (is_null($option)) {
$config = config('imap.options.fetch_body', true);
$this->fetch_body = is_bool($config) ? $config : true;
}
return $this;
}
/**
* Fail proof setter for $fetch_attachment.
*
* @param $option
*
* @return $this
*/
public function setFetchAttachmentOption($option)
{
if (is_bool($option)) {
$this->fetch_attachment = $option;
} elseif (is_null($option)) {
$config = config('imap.options.fetch_attachment', true);
$this->fetch_attachment = is_bool($config) ? $config : true;
}
return $this;
}
/**
* Fail proof setter for $fetch_flags.
*
* @param $option
*
* @return $this
*/
public function setFetchFlagsOption($option)
{
if (is_bool($option)) {
$this->fetch_flags = $option;
} elseif (is_null($option)) {
$config = config('imap.options.fetch_flags', true);
$this->fetch_flags = is_bool($config) ? $config : true;
}
return $this;
}
/**
* Decode a given string.
*
* @param $string
* @param $encoding
*
* @return string
*/
public function decodeString($string, $encoding)
{
switch ($encoding) {
case self::ENC_7BIT:
return $string;
case self::ENC_8BIT:
return quoted_printable_decode(imap_8bit($string));
case self::ENC_BINARY:
return imap_binary($string);
case self::ENC_BASE64:
return imap_base64($string);
case self::ENC_QUOTED_PRINTABLE:
return quoted_printable_decode($string);
case self::ENC_OTHER:
return $string;
default:
return $string;
}
}
/**
* Convert the encoding.
*
* @param $str
* @param string $from
* @param string $to
*
* @return mixed|string
*/
public function convertEncoding($str, $from = 'ISO-8859-2', $to = 'UTF-8')
{
// FreeScout fix
// 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;
}
if (function_exists('iconv') && $from != 'UTF-7' && $to != 'UTF-7') {
return iconv($from, $to.'//IGNORE', $str);
} else {
if (!$from) {
return mb_convert_encoding($str, $to);
}
return mb_convert_encoding($str, $to, $from);
}
}
/**
* Get the encoding of a given abject.
*
* @param object|string $structure
*
* @return string
*/
public function getEncoding($structure)
{
if (property_exists($structure, 'parameters')) {
foreach ($structure->parameters as $parameter) {
if (strtolower($parameter->attribute) == 'charset') {
return EncodingAliases::get($parameter->value);
}
}
} elseif (is_string($structure) === true) {
return mb_detect_encoding($structure);
}
return 'UTF-8';
}
/**
* Find the folder containing this message.
*
* @param null|Folder $folder where to start searching from (top-level inbox by default)
*
* @throws Exceptions\ConnectionFailedException
*
* @return null|Folder
*/
public function getContainingFolder(Folder $folder = null)
{
$folder = $folder ?: $this->client->getFolders()->first();
$this->client->checkConnection();
// Try finding the message by uid in the current folder
$client = new Client();
$client->openFolder($folder);
$uidMatches = imap_fetch_overview($client->getConnection(), $this->uid, FT_UID);
$uidMatch = count($uidMatches)
? new self($uidMatches[0]->uid, $uidMatches[0]->msgno, $client)
: null;
$client->disconnect();
// imap_fetch_overview() on a parent folder will return the matching message
// even when the message is in a child folder so we need to recursively
// search the children
foreach ($folder->children as $child) {
$childFolder = $this->getContainingFolder($child);
if ($childFolder) {
return $childFolder;
}
}
// before returning the parent
if ($this->is($uidMatch)) {
return $folder;
}
// or signalling that the message was not found in any folder
}
/**
* Move the Message into an other Folder.
*
* @param string $mailbox
*
* @throws Exceptions\ConnectionFailedException
*
* @return bool
*/
public function moveToFolder($mailbox = 'INBOX')
{
$this->client->createFolder($mailbox);
return imap_mail_move($this->client->getConnection(), $this->uid, $mailbox, CP_UID);
}
/**
* Delete the current Message.
*
* @param bool $expunge
*
* @throws Exceptions\ConnectionFailedException
*
* @return bool
*/
public function delete($expunge = true)
{
$status = imap_delete($this->client->getConnection(), $this->uid, FT_UID);
if ($expunge) {
$this->client->expunge();
}
return $status;
}
/**
* Restore a deleted Message.
*
* @param bool $expunge
*
* @throws Exceptions\ConnectionFailedException
*
* @return bool
*/
public function restore($expunge = true)
{
$status = imap_undelete($this->client->getConnection(), $this->uid, FT_UID);
if ($expunge) {
$this->client->expunge();
}
return $status;
}
/**
* Get all message attachments.
*
* @return AttachmentCollection
*/
public function getAttachments()
{
return $this->attachments;
}
/**
* Checks if there are any attachments present.
*
* @return bool
*/
public function hasAttachments()
{
return $this->attachments->isEmpty() === false;
}
/**
* Set a given flag.
*
* @param string|array $flag
*
* @throws Exceptions\ConnectionFailedException
*
* @return bool
*/
public function setFlag($flag)
{
$flag = '\\'.trim(is_array($flag) ? implode(' \\', $flag) : $flag);
$status = imap_setflag_full($this->client->getConnection(), $this->getUid(), $flag, SE_UID);
$this->parseFlags();
return $status;
}
/**
* Unset a given flag.
*
* @param string|array $flag
*
* @throws Exceptions\ConnectionFailedException
*
* @return bool
*/
public function unsetFlag($flag)
{
$flag = '\\'.trim(is_array($flag) ? implode(' \\', $flag) : $flag);
$status = imap_clearflag_full($this->client->getConnection(), $this->getUid(), $flag, SE_UID);
$this->parseFlags();
return $status;
}
/**
* @throws Exceptions\ConnectionFailedException
*
* @return null|object|string
*/
public function getRawBody()
{
if ($this->raw_body === null) {
$this->raw_body = imap_fetchbody($this->client->getConnection(), $this->getUid(), '', $this->fetch_options | FT_UID);
}
return $this->raw_body;
}
/**
* @return string
*/
public function getHeader()
{
return $this->header;
}
/**
* @return Client
*/
public function getClient()
{
return $this->client;
}
/**
* @return int
*/
public function getUid()
{
return $this->uid;
}
/**
* @return int
*/
public function getFetchOptions()
{
return $this->fetch_options;
}
/**
* @return bool
*/
public function getFetchBodyOption()
{
return $this->fetch_body;
}
/**
* @return int
*/
public function getPriority()
{
return $this->priority;
}
/**
* @return bool
*/
public function getFetchAttachmentOption()
{
return $this->fetch_attachment;
}
/**
* @return bool
*/
public function getFetchFlagsOption()
{
return $this->fetch_flags;
}
/**
* @return int
*/
public function getMsglist()
{
return $this->msglist;
}
/**
* @return mixed
*/
public function getMessageId()
{
return $this->message_id;
}
/**
* @return int
*/
public function getMessageNo()
{
return $this->message_no;
}
/**
* @return string
*/
public function getSubject()
{
return $this->subject;
}
/**
* @return mixed
*/
public function getReferences()
{
return $this->references;
}
/**
* @return Carbon|null
*/
public function getDate()
{
return $this->date;
}
/**
* @return array
*/
public function getFrom()
{
return $this->from;
}
/**
* @return array
*/
public function getTo()
{
return $this->to;
}
/**
* @return array
*/
public function getCc()
{
return $this->cc;
}
/**
* @return array
*/
public function getBcc()
{
return $this->bcc;
}
/**
* @return array
*/
public function getReplyTo()
{
return $this->reply_to;
}
/**
* @return string
*/
public function getInReplyTo()
{
return $this->in_reply_to;
}
/**
* @return array
*/
public function getSender()
{
return $this->sender;
}
/**
* @return mixed
*/
public function getBodies()
{
return $this->bodies;
}
/**
* @return FlagCollection
*/
public function getFlags()
{
return $this->flags;
}
/**
* Does this message match another one?
*
* A match means same uid, message id, subject and date/time.
*
* @param null|static $message
*
* @return bool
*/
public function is(self $message = null)
{
if (is_null($message)) {
return false;
}
return $this->uid == $message->uid
&& $this->message_id == $message->message_id
&& $this->subject == $message->subject
&& $this->date->eq($message->date);
}
}