Spaces:
No application file
No application file
namespace Mautic\EmailBundle\MonitoredEmail; | |
use Mautic\CoreBundle\Helper\CoreParametersHelper; | |
use Mautic\CoreBundle\Helper\PathsHelper; | |
use Mautic\EmailBundle\Exception\MailboxException; | |
use Mautic\EmailBundle\MonitoredEmail\Exception\NotConfiguredException; | |
class Mailbox | |
{ | |
/** | |
* Return all mails matching the rest of the criteria. | |
*/ | |
public const CRITERIA_ALL = 'ALL'; | |
/** | |
* Match mails with the \\ANSWERED flag set. | |
*/ | |
public const CRITERIA_ANSWERED = 'ANSWERED'; | |
/** | |
* CRITERIA_BCC "string" - match mails with "string" in the Bcc: field. | |
*/ | |
public const CRITERIA_BCC = 'BCC'; | |
/** | |
* CRITERIA_BEFORE "date" - match mails with Date: before "date". | |
*/ | |
public const CRITERIA_BEFORE = 'BEFORE'; | |
/** | |
* CRITERIA_BODY "string" - match mails with "string" in the body of the mail. | |
*/ | |
public const CRITERIA_BODY = 'BODY'; | |
/** | |
* CRITERIA_CC "string" - match mails with "string" in the Cc: field. | |
*/ | |
public const CRITERIA_CC = 'CC'; | |
/** | |
* Match deleted mails. | |
*/ | |
public const CRITERIA_DELETED = 'DELETED'; | |
/** | |
* Match mails with the \\FLAGGED (sometimes referred to as Important or Urgent) flag set. | |
*/ | |
public const CRITERIA_FLAGGED = 'FLAGGED'; | |
/** | |
* CRITERIA_FROM "string" - match mails with "string" in the From: field. | |
*/ | |
public const CRITERIA_FROM = 'FROM'; | |
/** | |
* CRITERIA_KEYWORD "string" - match mails with "string" as a keyword. | |
*/ | |
public const CRITERIA_KEYWORD = 'KEYWORD'; | |
/** | |
* Match new mails. | |
*/ | |
public const CRITERIA_NEW = 'NEW'; | |
/** | |
* Match old mails. | |
*/ | |
public const CRITERIA_OLD = 'OLD'; | |
/** | |
* CRITERIA_ON "date" - match mails with Date: matching "date". | |
*/ | |
public const CRITERIA_ON = 'ON'; | |
/** | |
* Match mails with the \\RECENT flag set. | |
*/ | |
public const CRITERIA_RECENT = 'RECENT'; | |
/** | |
* Match mails that have been read (the \\SEEN flag is set). | |
*/ | |
public const CRITERIA_SEEN = 'SEEN'; | |
/** | |
* CRITERIA_SINCE "date" - match mails with Date: after "date". | |
*/ | |
public const CRITERIA_SINCE = 'SINCE'; | |
/** | |
* CRITERIA_SUBJECT "string" - match mails with "string" in the Subject:. | |
*/ | |
public const CRITERIA_SUBJECT = 'SUBJECT'; | |
/** | |
* CRITERIA_TEXT "string" - match mails with text "string". | |
*/ | |
public const CRITERIA_TEXT = 'TEXT'; | |
/** | |
* CRITERIA_TO "string" - match mails with "string" in the To:. | |
*/ | |
public const CRITERIA_TO = 'TO'; | |
/** | |
* Get messages since a specific UID. Eg. UID 2:* will return all messages with UID 2 and above (IMAP includes the given UID). | |
*/ | |
public const CRITERIA_UID = 'UID'; | |
/** | |
* Match mails that have not been answered. | |
*/ | |
public const CRITERIA_UNANSWERED = 'UNANSWERED'; | |
/** | |
* Match mails that are not deleted. | |
*/ | |
public const CRITERIA_UNDELETED = 'UNDELETED'; | |
/** | |
* Match mails that are not flagged. | |
*/ | |
public const CRITERIA_UNFLAGGED = 'UNFLAGGED'; | |
/** | |
* CRITERIA_UNKEYWORD "string" - match mails that do not have the keyword "string". | |
*/ | |
public const CRITERIA_UNKEYWORD = 'UNKEYWORD'; | |
/** | |
* Match mails which have not been read yet. | |
*/ | |
public const CRITERIA_UNSEEN = 'UNSEEN'; | |
/** | |
* Match mails which have not been read yet - alias of CRITERIA_UNSEEN. | |
*/ | |
public const CRITERIA_UNREAD = 'UNSEEN'; | |
protected $imapPath; | |
protected $imapFullPath; | |
protected $imapStream; | |
protected $imapFolder = 'INBOX'; | |
protected $imapOptions = 0; | |
protected $imapRetriesNum = 0; | |
protected $imapParams = []; | |
protected $serverEncoding = 'UTF-8'; | |
protected $attachmentsDir; | |
protected $settings; | |
protected $isGmail = false; | |
protected $mailboxes; | |
/** | |
* @var mixed[] | |
*/ | |
private array $folders = []; | |
public function __construct(CoreParametersHelper $parametersHelper, PathsHelper $pathsHelper) | |
{ | |
$this->mailboxes = $parametersHelper->get('monitored_email', []); | |
if (isset($this->mailboxes['general'])) { | |
$this->settings = $this->mailboxes['general']; | |
} else { | |
$this->settings = [ | |
'host' => '', | |
'port' => '', | |
'password' => '', | |
'user' => '', | |
'encryption' => '', | |
'use_attachments' => false, | |
]; | |
} | |
$this->createAttachmentsDir($pathsHelper); | |
if ('imap.gmail.com' == $this->settings['host']) { | |
$this->isGmail = true; | |
} | |
} | |
/** | |
* Returns if a mailbox is configured. | |
* | |
* @throws MailboxException | |
*/ | |
public function isConfigured($bundleKey = null, $folderKey = null): bool | |
{ | |
if (null !== $bundleKey) { | |
try { | |
$this->switchMailbox($bundleKey, $folderKey); | |
} catch (MailboxException) { | |
return false; | |
} | |
} | |
return | |
!empty($this->settings['host']) && !empty($this->settings['port']) && !empty($this->settings['user']) | |
&& !empty($this->settings['password']); | |
} | |
/** | |
* Switch to another configured monitored mailbox. | |
* | |
* @param string $mailbox | |
* | |
* @throws MailboxException | |
*/ | |
public function switchMailbox($bundle, $mailbox = ''): void | |
{ | |
$key = $bundle.(!empty($mailbox) ? '_'.$mailbox : ''); | |
if (isset($this->mailboxes[$key])) { | |
$this->settings = (!empty($this->mailboxes[$key]['override_settings'])) ? $this->mailboxes[$key] : $this->mailboxes['general']; | |
$this->imapFolder = $this->mailboxes[$key]['folder']; | |
$this->settings['folder'] = $this->mailboxes[$key]['folder']; | |
// Disconnect so that new mailbox settings are used | |
$this->disconnect(); | |
// Setup new connection | |
$this->setImapPath(); | |
} else { | |
throw new MailboxException($key.' not found'); | |
} | |
} | |
/** | |
* Returns if this is a Gmail connection. | |
* | |
* @return mixed | |
*/ | |
public function isGmail() | |
{ | |
return $this->isGmail(); | |
} | |
/** | |
* Set imap path based on mailbox settings. | |
*/ | |
public function setImapPath($settings = null): void | |
{ | |
if (null == $settings) { | |
$settings = $this->settings; | |
} | |
$paths = $this->getImapPath($settings); | |
$this->imapPath = $paths['path']; | |
$this->imapFullPath = $paths['full']; | |
} | |
public function getImapPath($settings): array | |
{ | |
if (!isset($settings['encryption'])) { | |
$settings['encryption'] = (!empty($settings['ssl'])) ? '/ssl' : ''; | |
} | |
$path = "{{$settings['host']}:{$settings['port']}/imap{$settings['encryption']}}"; | |
$fullPath = $path; | |
if (isset($settings['folder'])) { | |
$fullPath .= $settings['folder']; | |
} | |
return ['path' => $path, 'full' => $fullPath]; | |
} | |
/** | |
* Override mailbox settings. | |
*/ | |
public function setMailboxSettings(array $settings): void | |
{ | |
$this->settings = array_merge($this->settings, $settings); | |
$this->isGmail = ('imap.gmail.com' == $this->settings['host']); | |
$this->setImapPath(); | |
} | |
/** | |
* Get settings. | |
* | |
* @param string $mailbox | |
* | |
* @return mixed | |
* | |
* @throws MailboxException | |
*/ | |
public function getMailboxSettings($bundle = null, $mailbox = '') | |
{ | |
if (null == $bundle) { | |
return $this->settings; | |
} | |
$key = $bundle.(!empty($mailbox) ? '_'.$mailbox : ''); | |
if (isset($this->mailboxes[$key])) { | |
$settings = (!empty($this->mailboxes[$key]['override_settings'])) ? $this->mailboxes[$key] : $this->mailboxes['general']; | |
$settings['folder'] = $this->mailboxes[$key]['folder']; | |
$this->setImapPath($settings); | |
$imapPath = $this->getImapPath($settings); | |
$settings['imap_path'] = $imapPath['full']; | |
} else { | |
throw new MailboxException($key.' not found'); | |
} | |
return $settings; | |
} | |
/** | |
* Set custom connection arguments of imap_open method. See http://php.net/imap_open. | |
* | |
* @param int $options | |
* @param int $retriesNum | |
*/ | |
public function setConnectionArgs($options = 0, $retriesNum = 0, array $params = null): void | |
{ | |
$this->imapOptions = $options; | |
$this->imapRetriesNum = $retriesNum; | |
$this->imapParams = $params; | |
} | |
/** | |
* Switch to another box. | |
*/ | |
public function switchFolder($folder): void | |
{ | |
if ($folder != $this->imapFolder) { | |
$this->imapFullPath = $this->imapPath.$folder; | |
$this->imapFolder = $folder; | |
} | |
$this->getImapStream(); | |
} | |
/** | |
* Get IMAP mailbox connection stream. | |
* | |
* @return resource|null | |
*/ | |
public function getImapStream() | |
{ | |
if (!$this->isConnected()) { | |
$this->imapStream = $this->initImapStream(); | |
} else { | |
@imap_reopen($this->imapStream, $this->imapFullPath); | |
} | |
return $this->imapStream; | |
} | |
/** | |
* @return resource | |
* | |
* @throws MailboxException | |
*/ | |
protected function initImapStream() | |
{ | |
imap_timeout(IMAP_OPENTIMEOUT, 15); | |
imap_timeout(IMAP_CLOSETIMEOUT, 15); | |
imap_timeout(IMAP_READTIMEOUT, 15); | |
imap_timeout(IMAP_WRITETIMEOUT, 15); | |
$imapStream = @imap_open( | |
$this->imapFullPath, | |
$this->settings['user'], | |
$this->settings['password'], | |
$this->imapOptions, | |
$this->imapRetriesNum, | |
$this->imapParams | |
); | |
if (!$imapStream) { | |
throw new MailboxException(); | |
} | |
return $imapStream; | |
} | |
/** | |
* Check if the stream is connected. | |
*/ | |
protected function isConnected(): bool | |
{ | |
return $this->isConfigured() && $this->imapStream && is_resource($this->imapStream) && @imap_ping($this->imapStream); | |
} | |
/** | |
* Get information about the current mailbox. | |
* | |
* Returns the information in an object with following properties: | |
* Date - current system time formatted according to RFC2822 | |
* Driver - protocol used to access this mailbox: POP3, IMAP, NNTP | |
* Mailbox - the mailbox name | |
* Nmsgs - number of mails in the mailbox | |
* Recent - number of recent mails in the mailbox | |
* | |
* @return \stdClass | |
*/ | |
public function checkMailbox(): \stdClass|bool | |
{ | |
return imap_check($this->getImapStream()); | |
} | |
/** | |
* Creates a new mailbox specified by mailbox. | |
*/ | |
public function createMailbox(): bool | |
{ | |
return imap_createmailbox($this->getImapStream(), imap_utf7_encode($this->imapFullPath)); | |
} | |
/** | |
* Gets status information about the given mailbox. | |
* | |
* This function returns an object containing status information. | |
* The object has the following properties: messages, recent, unseen, uidnext, and uidvalidity. | |
* | |
* @return \stdClass if the box doesn't exist | |
*/ | |
public function statusMailbox(): \stdClass|bool | |
{ | |
return imap_status($this->getImapStream(), $this->imapFullPath, SA_ALL); | |
} | |
/** | |
* Gets listing the folders. | |
* | |
* This function returns an object containing listing the folders. | |
* The object has the following properties: messages, recent, unseen, uidnext, and uidvalidity. | |
* | |
* @return array listing the folders | |
*/ | |
public function getListingFolders() | |
{ | |
if (!$this->isConfigured()) { | |
throw new NotConfiguredException('mautic.email.config.monitored_email.not_configured'); | |
} | |
if (!isset($this->folders[$this->imapFullPath])) { | |
$tempFolders = @imap_list($this->getImapStream(), $this->imapPath, '*'); | |
if (!empty($tempFolders)) { | |
foreach ($tempFolders as $key => $folder) { | |
$folder = str_replace($this->imapPath, '', imap_utf8($folder)); | |
$tempFolders[$key] = $folder; | |
} | |
} else { | |
$tempFolders = []; | |
} | |
$this->folders[$this->imapFullPath] = $tempFolders; | |
} | |
return $this->folders[$this->imapFullPath]; | |
} | |
public function fetchUnread($folder = null): array | |
{ | |
if (null !== $folder) { | |
$this->switchFolder($folder); | |
} | |
return $this->searchMailBox(self::CRITERIA_UNSEEN); | |
} | |
/** | |
* This function performs a search on the mailbox currently opened in the given IMAP stream. | |
* For example, to match all unanswered mails sent by Mom, you'd use: "UNANSWERED FROM mom". | |
* Searches appear to be case insensitive. This list of criteria is from a reading of the UW | |
* c-client source code and may be incomplete or inaccurate (see also RFC2060, section 6.4.4). | |
* | |
* @param string $criteria String, delimited by spaces, in which the following keywords are allowed. Any multi-word arguments (e.g. FROM "joey | |
* smith") must be quoted. Results will match all criteria entries. | |
* ALL - return all mails matching the rest of the criteria | |
* ANSWERED - match mails with the \\ANSWERED flag set | |
* BCC "string" - match mails with "string" in the Bcc: field | |
* BEFORE "date" - match mails with Date: before "date" | |
* BODY "string" - match mails with "string" in the body of the mail | |
* CC "string" - match mails with "string" in the Cc: field | |
* DELETED - match deleted mails | |
* FLAGGED - match mails with the \\FLAGGED (sometimes referred to as Important or Urgent) flag set | |
* FROM "string" - match mails with "string" in the From: field | |
* KEYWORD "string" - match mails with "string" as a keyword | |
* NEW - match new mails | |
* OLD - match old mails | |
* ON "date" - match mails with Date: matching "date" | |
* RECENT - match mails with the \\RECENT flag set | |
* SEEN - match mails that have been read (the \\SEEN flag is set) | |
* SINCE "date" - match mails with Date: after "date" | |
* SUBJECT "string" - match mails with "string" in the Subject: | |
* TEXT "string" - match mails with text "string" | |
* TO "string" - match mails with "string" in the To: | |
* UNANSWERED - match mails that have not been answered | |
* UNDELETED - match mails that are not deleted | |
* UNFLAGGED - match mails that are not flagged | |
* UNKEYWORD "string" - match mails that do not have the keyword "string" | |
* UNSEEN - match mails which have not been read yet | |
* | |
* @return array Mails ids | |
*/ | |
public function searchMailbox($criteria = self::CRITERIA_ALL): array | |
{ | |
if (preg_match('/'.self::CRITERIA_UID.' ((\d+):(\d+|\*))/', $criteria, $matches)) { | |
// PHP imap_search does not support UID n:* so use imap_fetch_overview instead | |
$messages = imap_fetch_overview($this->getImapStream(), $matches[1], FT_UID); | |
$mailIds = []; | |
foreach ($messages as $message) { | |
$mailIds[] = $message->uid; | |
} | |
} else { | |
$mailIds = imap_search($this->getImapStream(), $criteria, SE_UID); | |
} | |
return $mailIds ?: []; | |
} | |
/** | |
* Save mail body. | |
* | |
* @param string $filename | |
*/ | |
public function saveMail($mailId, $filename = 'email.eml'): bool | |
{ | |
return imap_savebody($this->getImapStream(), $filename, $mailId, '', FT_UID); | |
} | |
/** | |
* Marks mails listed in mailId for deletion. | |
*/ | |
public function deleteMail($mailId): bool | |
{ | |
return imap_delete($this->getImapStream(), $mailId, FT_UID); | |
} | |
/** | |
* Move mail to another box. | |
*/ | |
public function moveMail($mailId, $mailBox): bool | |
{ | |
return imap_mail_move($this->getImapStream(), $mailId, $mailBox, CP_UID) && $this->expungeDeletedMails(); | |
} | |
/** | |
* Deletes all the mails marked for deletion by imap_delete(), imap_mail_move(), or imap_setflag_full(). | |
*/ | |
public function expungeDeletedMails(): bool | |
{ | |
return imap_expunge($this->getImapStream()); | |
} | |
/** | |
* Add the flag \Seen to a mail. | |
*/ | |
public function markMailAsRead($mailId): bool | |
{ | |
return $this->setFlag([$mailId], '\\Seen'); | |
} | |
/** | |
* Remove the flag \Seen from a mail. | |
*/ | |
public function markMailAsUnread($mailId): bool | |
{ | |
return $this->clearFlag([$mailId], '\\Seen'); | |
} | |
/** | |
* Add the flag \Flagged to a mail. | |
*/ | |
public function markMailAsImportant($mailId): bool | |
{ | |
return $this->setFlag([$mailId], '\\Flagged'); | |
} | |
/** | |
* Add the flag \Seen to a mails. | |
*/ | |
public function markMailsAsRead(array $mailIds): bool | |
{ | |
return $this->setFlag($mailIds, '\\Seen'); | |
} | |
/** | |
* Remove the flag \Seen from some mails. | |
*/ | |
public function markMailsAsUnread(array $mailIds): bool | |
{ | |
return $this->clearFlag($mailIds, '\\Seen'); | |
} | |
/** | |
* Add the flag \Flagged to some mails. | |
*/ | |
public function markMailsAsImportant(array $mailIds): bool | |
{ | |
return $this->setFlag($mailIds, '\\Flagged'); | |
} | |
/** | |
* Causes a store to add the specified flag to the flags set for the mails in the specified sequence. | |
* | |
* @param string $flag which you can set are \Seen, \Answered, \Flagged, \Deleted, and \Draft as defined by RFC2060 | |
*/ | |
public function setFlag(array $mailsIds, $flag): bool | |
{ | |
return imap_setflag_full($this->getImapStream(), implode(',', $mailsIds), $flag, ST_UID); | |
} | |
/** | |
* Cause a store to delete the specified flag to the flags set for the mails in the specified sequence. | |
* | |
* @param string $flag which you can set are \Seen, \Answered, \Flagged, \Deleted, and \Draft as defined by RFC2060 | |
*/ | |
public function clearFlag(array $mailsIds, $flag): bool | |
{ | |
return imap_clearflag_full($this->getImapStream(), implode(',', $mailsIds), $flag, ST_UID); | |
} | |
/** | |
* Fetch mail headers for listed mails ids. | |
* | |
* Returns an array of objects describing one mail header each. The object will only define a property if it exists. The possible properties are: | |
* subject - the mails subject | |
* from - who sent it | |
* to - recipient | |
* date - when was it sent | |
* message_id - Mail-ID | |
* references - is a reference to this mail id | |
* in_reply_to - is a reply to this mail id | |
* size - size in bytes | |
* uid - UID the mail has in the mailbox | |
* msgno - mail sequence number in the mailbox | |
* recent - this mail is flagged as recent | |
* flagged - this mail is flagged | |
* answered - this mail is flagged as answered | |
* deleted - this mail is flagged for deletion | |
* seen - this mail is flagged as already read | |
* draft - this mail is flagged as being a draft | |
* | |
* @return array | |
*/ | |
public function getMailsInfo(array $mailsIds) | |
{ | |
$mails = imap_fetch_overview($this->getImapStream(), implode(',', $mailsIds), FT_UID); | |
if (is_array($mails) && count($mails)) { | |
foreach ($mails as &$mail) { | |
if (isset($mail->subject)) { | |
$mail->subject = $this->decodeMimeStr($mail->subject, $this->serverEncoding); | |
} | |
if (isset($mail->from)) { | |
$mail->from = $this->decodeMimeStr($mail->from, $this->serverEncoding); | |
} | |
if (isset($mail->to)) { | |
$mail->to = $this->decodeMimeStr($mail->to, $this->serverEncoding); | |
} | |
} | |
} | |
return $mails; | |
} | |
/** | |
* Get information about the current mailbox. | |
* | |
* Returns an object with following properties: | |
* Date - last change (current datetime) | |
* Driver - driver | |
* Mailbox - name of the mailbox | |
* Nmsgs - number of messages | |
* Recent - number of recent messages | |
* Unread - number of unread messages | |
* Deleted - number of deleted messages | |
* Size - mailbox size | |
*/ | |
public function getMailboxInfo(): \stdClass | |
{ | |
return imap_mailboxmsginfo($this->getImapStream()); | |
} | |
/** | |
* Gets mails ids sorted by some criteria. | |
* | |
* Criteria can be one (and only one) of the following constants: | |
* SORTDATE - mail Date | |
* SORTARRIVAL - arrival date (default) | |
* SORTFROM - mailbox in first From address | |
* SORTSUBJECT - mail subject | |
* SORTTO - mailbox in first To address | |
* SORTCC - mailbox in first cc address | |
* SORTSIZE - size of mail in octets | |
* | |
* @param int $criteria | |
* @param bool $reverse | |
* | |
* @return array Mails ids | |
*/ | |
public function sortMails($criteria = SORTARRIVAL, $reverse = true): array|bool | |
{ | |
return imap_sort($this->getImapStream(), $criteria, $reverse, SE_UID); | |
} | |
/** | |
* Get mails count in mail box. | |
*/ | |
public function countMails(): int|bool | |
{ | |
return imap_num_msg($this->getImapStream()); | |
} | |
/** | |
* Retrieve the quota settings per user. | |
* | |
* @return array - FALSE in the case of call failure | |
*/ | |
protected function getQuota(): array|bool | |
{ | |
return imap_get_quotaroot($this->getImapStream(), 'INBOX'); | |
} | |
/** | |
* Return quota limit in KB. | |
* | |
* @return int - FALSE in the case of call failure | |
*/ | |
public function getQuotaLimit() | |
{ | |
$quota = $this->getQuota(); | |
if (is_array($quota)) { | |
$quota = $quota['STORAGE']['limit']; | |
} | |
return $quota; | |
} | |
/** | |
* Return quota usage in KB. | |
* | |
* @return int - FALSE in the case of call failure | |
*/ | |
public function getQuotaUsage() | |
{ | |
$quota = $this->getQuota(); | |
if (is_array($quota)) { | |
$quota = $quota['STORAGE']['usage']; | |
} | |
return $quota; | |
} | |
/** | |
* Get mail data. | |
* | |
* @param bool $markAsSeen | |
*/ | |
public function getMail($mailId, $markAsSeen = true): Message | |
{ | |
$header = imap_fetchheader($this->getImapStream(), $mailId, FT_UID); | |
$headObject = imap_rfc822_parse_headers($header); | |
$mail = new Message(); | |
$mail->id = $mailId; | |
$mail->date = date('Y-m-d H:i:s', isset($headObject->date) ? strtotime(preg_replace('/\(.*?\)/', '', $headObject->date)) : time()); | |
$mail->subject = isset($headObject->subject) ? $this->decodeMimeStr($headObject->subject, $this->serverEncoding) : null; | |
$mail->fromName = isset($headObject->from[0]->personal) ? $this->decodeMimeStr($headObject->from[0]->personal, $this->serverEncoding) | |
: null; | |
$mail->fromAddress = strtolower($headObject->from[0]->mailbox.'@'.$headObject->from[0]->host); | |
if (isset($headObject->to)) { | |
$toStrings = []; | |
foreach ($headObject->to as $to) { | |
if (!empty($to->mailbox) && !empty($to->host)) { | |
$toEmail = strtolower($to->mailbox.'@'.$to->host); | |
$toName = isset($to->personal) ? $this->decodeMimeStr($to->personal, $this->serverEncoding) : null; | |
$toStrings[] = $toName ? "$toName <$toEmail>" : $toEmail; | |
$mail->to[$toEmail] = $toName; | |
} | |
} | |
$mail->toString = implode(', ', $toStrings); | |
} | |
if (isset($headObject->cc)) { | |
foreach ($headObject->cc as $cc) { | |
$mail->cc[strtolower($cc->mailbox.'@'.$cc->host)] = isset($cc->personal) ? $this->decodeMimeStr($cc->personal, $this->serverEncoding) | |
: null; | |
} | |
} | |
if (isset($headObject->reply_to)) { | |
foreach ($headObject->reply_to as $replyTo) { | |
$mail->replyTo[strtolower($replyTo->mailbox.'@'.$replyTo->host)] = isset($replyTo->personal) ? $this->decodeMimeStr( | |
$replyTo->personal, | |
$this->serverEncoding | |
) : null; | |
} | |
} | |
if (isset($headObject->in_reply_to)) { | |
$mail->inReplyTo = $headObject->in_reply_to; | |
} | |
if (isset($headObject->return_path)) { | |
$mail->returnPath = $headObject->return_path; | |
} | |
if (isset($headObject->references)) { | |
$mail->references = explode("\n", $headObject->references); | |
} | |
$mailStructure = imap_fetchstructure($this->getImapStream(), $mailId, FT_UID); | |
if (empty($mailStructure->parts)) { | |
$this->initMailPart($mail, $mailStructure, 0, $markAsSeen); | |
} else { | |
foreach ($mailStructure->parts as $partNum => $partStructure) { | |
$this->initMailPart($mail, $partStructure, $partNum + 1, $markAsSeen); | |
} | |
} | |
// Parse X headers | |
$tempArray = explode("\n", $header); | |
$headers = []; | |
foreach ($tempArray as $line) { | |
if (preg_match('/^X-(.*?): (.*?)$/is', trim($line), $matches)) { | |
$headers['x-'.strtolower($matches[1])] = $matches[2]; | |
} | |
} | |
$mail->xHeaders = $headers; | |
return $mail; | |
} | |
/** | |
* @param bool|true $markAsSeen | |
* @param bool|false $isDsn | |
* @param bool|false $isFbl | |
*/ | |
protected function initMailPart(Message $mail, $partStructure, $partNum, $markAsSeen = true, $isDsn = false, $isFbl = false) | |
{ | |
$options = FT_UID; | |
if (!$markAsSeen) { | |
$options |= FT_PEEK; | |
} | |
$data = $partNum | |
? imap_fetchbody($this->getImapStream(), $mail->id, $partNum, $options) | |
: imap_body( | |
$this->getImapStream(), | |
$mail->id, | |
$options | |
); | |
if (1 == $partStructure->encoding) { | |
$data = imap_utf8($data); | |
} elseif (2 == $partStructure->encoding) { | |
$data = imap_binary($data); | |
} elseif (3 == $partStructure->encoding) { | |
$data = imap_base64($data); | |
} elseif (4 == $partStructure->encoding) { | |
$data = quoted_printable_decode($data); | |
} | |
$params = $this->getParameters($partStructure); | |
// attachments | |
$attachmentId = $partStructure->ifid | |
? trim($partStructure->id, ' <>') | |
: (isset($params['filename']) || isset($params['name']) ? mt_rand().mt_rand() : null); | |
// ignore contentId on body when mail isn't multipart (https://github.com/barbushin/php-imap/issues/71) | |
if (!$partNum && TYPETEXT === $partStructure->type) { | |
$attachmentId = null; | |
} | |
if ($attachmentId) { | |
if (isset($this->settings['use_attachments']) && $this->settings['use_attachments']) { | |
if (empty($params['filename']) && empty($params['name'])) { | |
$fileName = $attachmentId.'.'.strtolower($partStructure->subtype); | |
} else { | |
$fileName = !empty($params['filename']) ? $params['filename'] : $params['name']; | |
$fileName = $this->decodeMimeStr($fileName, $this->serverEncoding); | |
$fileName = $this->decodeRFC2231($fileName, $this->serverEncoding); | |
} | |
$attachment = new Attachment(); | |
$attachment->id = $attachmentId; | |
$attachment->name = $fileName; | |
if ($this->attachmentsDir) { | |
$replace = [ | |
'/\s/' => '_', | |
'/[^0-9a-zа-яіїє_\.]/iu' => '', | |
'/_+/' => '_', | |
'/(^_)|(_$)/' => '', | |
]; | |
$fileSysName = preg_replace( | |
'~[\\\\/]~', | |
'', | |
$mail->id.'_'.$attachmentId.'_'.preg_replace(array_keys($replace), $replace, $fileName) | |
); | |
$attachment->filePath = $this->attachmentsDir.DIRECTORY_SEPARATOR.$fileSysName; | |
file_put_contents($attachment->filePath, $data); | |
} | |
$mail->addAttachment($attachment); | |
} | |
} else { | |
if (!empty($params['charset'])) { | |
$data = $this->convertStringEncoding($data, $params['charset'], $this->serverEncoding); | |
} | |
if (!empty($data)) { | |
$subtype = !empty($partStructure->ifsubtype) | |
? strtolower($partStructure->subtype) | |
: ''; | |
switch ($partStructure->type) { | |
case TYPETEXT: | |
match ($subtype) { | |
'plain' => $mail->textPlain .= $data, | |
// no break | |
default => $mail->textHtml .= $data, | |
}; | |
break; | |
case TYPEMULTIPART: | |
if ( | |
'report' != $subtype | |
|| empty($params['report-type']) | |
) { | |
break; | |
} | |
$reportType = strtolower($params['report-type']); | |
switch ($reportType) { | |
case 'delivery-status': | |
$mail->dsnMessage = trim($data); | |
$isDsn = true; | |
break; | |
case 'feedback-report': | |
$mail->fblMessage = trim($data); | |
$isFbl = true; | |
break; | |
default: | |
// Just pass through. | |
} | |
break; | |
case TYPEMESSAGE: | |
if ($isDsn || ('delivery-status' == $subtype)) { | |
$mail->dsnReport = $data; | |
} elseif ($isFbl || ('feedback-report' == $subtype)) { | |
$mail->fblReport = $data; | |
} else { | |
$mail->textPlain .= trim($data); | |
} | |
break; | |
default: | |
// Just pass through. | |
} | |
} | |
} | |
if (!empty($partStructure->parts)) { | |
foreach ($partStructure->parts as $subPartNum => $subPartStructure) { | |
if (2 == $partStructure->type && 'RFC822' == $partStructure->subtype) { | |
$this->initMailPart($mail, $subPartStructure, $partNum, $markAsSeen, $isDsn, $isFbl); | |
} else { | |
$this->initMailPart($mail, $subPartStructure, $partNum.'.'.($subPartNum + 1), $markAsSeen, $isDsn, $isFbl); | |
} | |
} | |
} | |
} | |
protected function getParameters($partStructure): array | |
{ | |
$params = []; | |
if (!empty($partStructure->parameters)) { | |
foreach ($partStructure->parameters as $param) { | |
$params[strtolower($param->attribute)] = $param->value; | |
} | |
} | |
if (!empty($partStructure->dparameters)) { | |
foreach ($partStructure->dparameters as $param) { | |
$paramName = strtolower(preg_match('~^(.*?)\*~', $param->attribute, $matches) ? $matches[1] : $param->attribute); | |
if (isset($params[$paramName])) { | |
$params[$paramName] .= $param->value; | |
} else { | |
$params[$paramName] = $param->value; | |
} | |
} | |
} | |
return $params; | |
} | |
/** | |
* @param string $charset | |
*/ | |
protected function decodeMimeStr($string, $charset = 'utf-8'): string | |
{ | |
$newString = ''; | |
$elements = imap_mime_header_decode($string); | |
for ($i = 0; $i < count($elements); ++$i) { | |
if ('default' == $elements[$i]->charset) { | |
$elements[$i]->charset = 'iso-8859-1'; | |
} | |
$newString .= $this->convertStringEncoding($elements[$i]->text, $elements[$i]->charset, $charset); | |
} | |
return $newString; | |
} | |
protected function isUrlEncoded($string): bool | |
{ | |
$hasInvalidChars = preg_match('#[^%a-zA-Z0-9\-_\.\+]#', $string); | |
$hasEscapedChars = preg_match('#%[a-zA-Z0-9]{2}#', $string); | |
return !$hasInvalidChars && $hasEscapedChars; | |
} | |
/** | |
* @param string $charset | |
* | |
* @return string | |
*/ | |
protected function decodeRFC2231($string, $charset = 'utf-8') | |
{ | |
if (preg_match("/^(.*?)'.*?'(.*?)$/", $string, $matches)) { | |
$encoding = $matches[1]; | |
$data = $matches[2]; | |
if ($this->isUrlEncoded($data)) { | |
$string = $this->convertStringEncoding(urldecode($data), $encoding, $charset); | |
} | |
} | |
return $string; | |
} | |
/** | |
* Converts a string from one encoding to another. | |
* | |
* @param string $string | |
* @param string $fromEncoding | |
* @param string $toEncoding | |
* | |
* @return string Converted string if conversion was successful, or the original string if not | |
*/ | |
protected function convertStringEncoding($string, $fromEncoding, $toEncoding) | |
{ | |
$convertedString = null; | |
if ($string && $fromEncoding != $toEncoding) { | |
$convertedString = @iconv($fromEncoding, $toEncoding.'//IGNORE', $string); | |
if (!$convertedString && extension_loaded('mbstring')) { | |
$convertedString = @mb_convert_encoding($string, $toEncoding, $fromEncoding); | |
} | |
} | |
return $convertedString ?: $string; | |
} | |
/** | |
* Close IMAP connection. | |
*/ | |
protected function disconnect() | |
{ | |
if ($this->isConnected()) { | |
// Prevent these from throwing notices such as "SECURITY PROBLEM: insecure server advertised" | |
imap_errors(); | |
imap_alerts(); | |
@imap_close($this->imapStream, CL_EXPUNGE); | |
} | |
} | |
private function createAttachmentsDir(PathsHelper $pathsHelper): void | |
{ | |
if (!isset($this->settings['use_attachments']) || !$this->settings['use_attachments']) { | |
return; | |
} | |
$this->attachmentsDir = $pathsHelper->getSystemPath('tmp', true); | |
if (!file_exists($this->attachmentsDir)) { | |
mkdir($this->attachmentsDir); | |
} | |
$this->attachmentsDir .= '/attachments'; | |
if (!file_exists($this->attachmentsDir)) { | |
mkdir($this->attachmentsDir); | |
} | |
} | |
/** | |
* Disconnect on destruct. | |
*/ | |
public function __destruct() | |
{ | |
$this->disconnect(); | |
} | |
} | |