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(); | |
| } | |
| } | |