Skip to content

Commit

Permalink
Mainly fixed decoding issues
Browse files Browse the repository at this point in the history
- Updated comments, made code a bit more dynamic, used setAttachmentsDir() in constructor
- Added condition for setting attachments dir in constructor
- Issue #232: Added test strings with emojis (smiles) to PHPUnit test
- Issue #232: Added strings to test decodeMimeStr() in PHPUnit test with non-UTF8 server encoding
- Issue #232: Fixed broken UTF-8 decoded strings, when not using UTF-8 as server encoding
- Issue #265: Added string to test iconv() in PHPUnit test
- Issue #274: Some strings with default charset had UTF-8 characters and failed to convert
  • Loading branch information
Sebbo94BY committed May 11, 2019
2 parents 75e203b + 04e01b9 commit ab18be7
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 55 deletions.
11 changes: 10 additions & 1 deletion src/PhpImap/DataPartInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ function fetch() {
}

switch($this->encoding) {
case ENC7BIT:
$this->data = $this->data;
break;
case ENC8BIT:
$this->data = imap_utf8($this->data);
break;
Expand All @@ -49,10 +52,16 @@ function fetch() {
case ENCQUOTEDPRINTABLE:
$this->data = quoted_printable_decode($this->data);
break;
case ENCOTHER:
$this->data = $this->data;
break;
default:
$this->data = $this->data;
break;
}

if(isset($this->charset)) {
$this->data = $this->mail->convertStringEncoding($this->data, $this->charset, $this->mail->getServerEncoding());
$this->data = $this->mail->decodeMimeStr($this->data, $this->charset);
}

return $this->data;
Expand Down
136 changes: 83 additions & 53 deletions src/PhpImap/Mailbox.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,8 @@ public function __construct($imapPath, $login, $password, $attachmentsDir = null
$this->imapLogin = trim($login);
$this->imapPassword = $password;
$this->setServerEncoding($serverEncoding);
if($attachmentsDir) {
if(!is_dir($attachmentsDir)) {
throw new InvalidParameterException('Directory "' . $attachmentsDir . '" not found');
}
$this->attachmentsDir = rtrim(realpath($attachmentsDir), '\\/');
if($attachmentsDir != null) {
$this->setAttachmentsDir($attachmentsDir);
}
}

Expand Down Expand Up @@ -167,6 +164,7 @@ public function getAttachmentsIgnore() {
* Sets the timeout of all or one specific type
* @param int $timeout Timeout in seconds
* @param array $types One of the following: IMAP_OPENTIMEOUT, IMAP_READTIMEOUT, IMAP_WRITETIMEOUT, IMAP_CLOSETIMEOUT
* @return void
* @throws InvalidParameterException
*/
public function setTimeouts($timeout, $types = [IMAP_OPENTIMEOUT, IMAP_READTIMEOUT, IMAP_WRITETIMEOUT, IMAP_CLOSETIMEOUT]) {
Expand Down Expand Up @@ -230,18 +228,42 @@ public function setConnectionArgs($options = 0, $retriesNum = 0, $params = NULL)
/**
* Set custom folder for attachments in case you want to have tree of folders for each email
* i.e. a/1 b/1 c/1 where a,b,c - senders, i.e. john@smith.com
* @param string $dir folder where to save attachments
*
* @param string $attachmentsDir Folder where to save attachments
* @return void
* @throws InvalidParameterException
*/
public function setAttachmentsDir($attachmentsDir) {
if(empty($attachmentsDir)) {
throw new InvalidParameterException('setAttachmentsDir() expects a string as first parameter!');
}
if(!is_dir($attachmentsDir)) {
throw new InvalidParameterException('Directory "' . $attachmentsDir . '" not found');
}
$this->attachmentsDir = rtrim(realpath($attachmentsDir), '\\/');
}

/**
* Get current saving folder for attachments
* @return string Attachments dir
*/
public function setAttachmentsDir($dir) {
$this->attachmentsDir = $dir;
public function getAttachmentsDir() {
return $this->attachmentsDir;
}

/*
* Sets / Changes the attempts / retries to connect
* @param int $maxAttempts
* @return void
*/
public function setConnectionRetry($maxAttempts) {
$this->connectionRetry = $maxAttempts;
}

/*
* Sets / Changes the delay between each attempt / retry to connect
* @param int $milliseconds
* @return void
*/
public function setConnectionRetryDelay($milliseconds) {
$this->connectionRetryDelay = $milliseconds;
}
Expand All @@ -267,8 +289,8 @@ public function getImapStream($forceConnection = true) {
/**
* Returns the provided string in UTF7-IMAP encoded format
*
* @param string $any_encoded_string
* @return string $utf7_encoded_string
* @param string $tr Any encoded string
* @return string $str UTF-7 encoded string or same as before, when it's no string
*/
public function encodeStringToUtf7Imap($str) {
if(is_string($str)) {
Expand All @@ -282,8 +304,8 @@ public function encodeStringToUtf7Imap($str) {
/**
* Returns the provided string in UTF-8 encoded format
*
* @param string $any_encoded_string
* @return string $utf7_encoded_string
* @param string $tr Any encoded string
* @return string $str UTF-7 encoded string or same as before, when it's no string
*/
public function decodeStringFromUtf7ImapToUtf8($str) {
if(is_string($str)) {
Expand Down Expand Up @@ -363,24 +385,24 @@ public function checkMailbox() {

/**
* Creates a new mailbox
* @param $name
* @param string $name Name of new mailbox (eg. 'PhpImap')
*/
public function createMailbox($name) {
$this->imap('createmailbox', $this->imapPath . $this->getPathDelimiter() . $name);
}

/**
* Delete mailbox
* @param $name
* Deletes a specific mailbox
* @param string $name Name of mailbox, which you want to delete (eg. 'PhpImap')
*/
public function deleteMailbox($name) {
$this->imap('deletemailbox', $this->imapPath . $this->getPathDelimiter() . $name);
}

/**
* Rename mailbox
* @param $oldName
* @param $newName
* Rename an existing mailbox from $oldName to $newName
* @param string $oldName Current name of mailbox, which you want to rename (eg. 'PhpImap')
* @param string $newName New name of mailbox, to which you want to rename it (eg. 'PhpImapTests')
*/
public function renameMailbox($oldName, $newName) {
$this->imap('renamemailbox', [$this->imapPath . $this->getPathDelimiter() . $oldName, $this->imapPath . $this->getPathDelimiter() . $newName]);
Expand Down Expand Up @@ -420,17 +442,18 @@ public function getListingFolders($pattern = '*') {
* For example, to match all unanswered mails sent by Mom, you'd use: "UNANSWERED FROM mom".
*
* @param string $criteria See http://php.net/imap_search for a complete list of available criteria
* @param boolean $disableServerEncoding Disables server encoding while searching for mails (can be useful on Exchange servers)
* @return array mailsIds (or empty array)
*/
public function searchMailbox($criteria = 'ALL', $disableServerEncoding = false) {
if($disableServerEncoding) {
return $this->imap('search', [$criteria, $this->imapSearchOption]) ?: [];
}
return $this->imap('search', [$criteria, $this->imapSearchOption, $this->serverEncoding]) ?: [];
return $this->imap('search', [$criteria, $this->imapSearchOption, $this->getServerEncoding()]) ?: [];
}

/**
* Save mail body.
* Save a specific body section to a file
* @param $mailId
* @param string $filename
*/
Expand Down Expand Up @@ -569,16 +592,16 @@ public function getMailsInfo(array $mailsIds) {
if(is_array($mails) && count($mails)) {
foreach($mails as &$mail) {
if(isset($mail->subject)) {
$mail->subject = $this->decodeMimeStr($mail->subject, $this->serverEncoding);
$mail->subject = $this->decodeMimeStr($mail->subject, $this->getServerEncoding());
}
if(isset($mail->from) AND !empty($head->from)) {
$mail->from = $this->decodeMimeStr($mail->from, $this->serverEncoding);
$mail->from = $this->decodeMimeStr($mail->from, $this->getServerEncoding());
}
if(isset($mail->sender) AND !empty($head->sender)) {
$mail->sender = $this->decodeMimeStr($mail->sender, $this->serverEncoding);
$mail->sender = $this->decodeMimeStr($mail->sender, $this->getServerEncoding());
}
if(isset($mail->to)) {
$mail->to = $this->decodeMimeStr($mail->to, $this->serverEncoding);
$mail->to = $this->decodeMimeStr($mail->to, $this->getServerEncoding());
}
}
}
Expand Down Expand Up @@ -647,35 +670,38 @@ public function countMails() {

/**
* Retrieve the quota settings per user
* @param string Should normally be in the form of which mailbox (i.e. INBOX)
* @return array
*/
protected function getQuota() {
return $this->imap('get_quotaroot', 'INBOX');
protected function getQuota($quota_root = 'INBOX') {
return $this->imap('get_quotaroot', $quota_root);
}

/**
* Return quota limit in KB
* @param string Should normally be in the form of which mailbox (i.e. INBOX)
* @return int
*/
public function getQuotaLimit() {
$quota = $this->getQuota();
public function getQuotaLimit($quota_root = 'INBOX') {
$quota = $this->getQuota($quota_root);
return isset($quota['STORAGE']['limit']) ? $quota['STORAGE']['limit'] : 0;
}

/**
* Return quota usage in KB
* @param string Should normally be in the form of which mailbox (i.e. INBOX)
* @return int FALSE in the case of call failure
*/
public function getQuotaUsage() {
$quota = $this->getQuota();
public function getQuotaUsage($quota_root = 'INBOX') {
$quota = $this->getQuota($quota_root);
return isset($quota['STORAGE']['usage']) ? $quota['STORAGE']['usage'] : 0;
}

/**
* Get raw mail data
*
* @param $msgId
* @param bool $markAsSeen
* @param bool $markAsSeen Mark the email as seen, when set to true
* @return mixed
*/
public function getRawMail($msgId, $markAsSeen = true) {
Expand Down Expand Up @@ -722,26 +748,26 @@ public function getMailHeader($mailId) {
$header->date = self::parseDateTime($now->format('Y-m-d H:i:s'));
}

$header->subject = (isset($head->subject) AND !empty($head->subject)) ? $this->decodeMimeStr($head->subject, $this->serverEncoding) : null;
$header->subject = (isset($head->subject) AND !empty($head->subject)) ? $this->decodeMimeStr($head->subject, $this->getServerEncoding()) : null;
if(isset($head->from) AND !empty($head->from)) {
$header->fromHost = isset($head->from[0]->host) ? $head->from[0]->host : (isset($head->from[1]->host) ? $head->from[1]->host : null);
$header->fromName = (isset($head->from[0]->personal) AND !empty($head->from[0]->personal)) ? $this->decodeMimeStr($head->from[0]->personal, $this->serverEncoding) : ((isset($head->from[1]->personal) AND (!empty($head->from[1]->personal))) ? $this->decodeMimeStr($head->from[1]->personal, $this->serverEncoding) : null);
$header->fromName = (isset($head->from[0]->personal) AND !empty($head->from[0]->personal)) ? $this->decodeMimeStr($head->from[0]->personal, $this->getServerEncoding()) : ((isset($head->from[1]->personal) AND (!empty($head->from[1]->personal))) ? $this->decodeMimeStr($head->from[1]->personal, $this->getServerEncoding()) : null);
$header->fromAddress = strtolower($head->from[0]->mailbox . '@' . $header->fromHost);
}
elseif(preg_match("/smtp.mailfrom=[-0-9a-zA-Z.+_]+@[-0-9a-zA-Z.+_]+.[a-zA-Z]{2,4}/", $headersRaw, $matches)) {
$header->fromAddress = substr($matches[0], 14);
}
if(isset($head->sender) AND !empty($head->sender)) {
$header->senderHost = isset($head->sender[0]->host) ? $head->sender[0]->host : (isset($head->sender[1]->host) ? $head->sender[1]->host : null);
$header->senderName = (isset($head->sender[0]->personal) AND !empty($head->sender[0]->personal)) ? $this->decodeMimeStr($head->sender[0]->personal, $this->serverEncoding) : ((isset($head->sender[1]->personal) AND (!empty($head->sender[1]->personal))) ? $this->decodeMimeStr($head->sender[1]->personal, $this->serverEncoding) : null);
$header->senderName = (isset($head->sender[0]->personal) AND !empty($head->sender[0]->personal)) ? $this->decodeMimeStr($head->sender[0]->personal, $this->getServerEncoding()) : ((isset($head->sender[1]->personal) AND (!empty($head->sender[1]->personal))) ? $this->decodeMimeStr($head->sender[1]->personal, $this->getServerEncoding()) : null);
$header->senderAddress = strtolower($head->sender[0]->mailbox . '@' . $header->senderHost);
}
if(isset($head->to)) {
$toStrings = [];
foreach($head->to as $to) {
if(!empty($to->mailbox) && !empty($to->host)) {
$toEmail = strtolower($to->mailbox . '@' . $to->host);
$toName = (isset($to->personal) AND !empty($to->personal)) ? $this->decodeMimeStr($to->personal, $this->serverEncoding) : null;
$toName = (isset($to->personal) AND !empty($to->personal)) ? $this->decodeMimeStr($to->personal, $this->getServerEncoding()) : null;
$toStrings[] = $toName ? "$toName <$toEmail>" : $toEmail;
$header->to[$toEmail] = $toName;
}
Expand All @@ -752,22 +778,22 @@ public function getMailHeader($mailId) {
if(isset($head->cc)) {
foreach($head->cc as $cc) {
if(!empty($cc->mailbox) && !empty($cc->host)) {
$header->cc[strtolower($cc->mailbox . '@' . $cc->host)] = (isset($cc->personal) AND !empty($cc->personal)) ? $this->decodeMimeStr($cc->personal, $this->serverEncoding) : null;
$header->cc[strtolower($cc->mailbox . '@' . $cc->host)] = (isset($cc->personal) AND !empty($cc->personal)) ? $this->decodeMimeStr($cc->personal, $this->getServerEncoding()) : null;
}
}
}

if(isset($head->bcc)) {
foreach($head->bcc as $bcc) {
if(!empty($bcc->mailbox) && !empty($bcc->host)) {
$header->bcc[strtolower($bcc->mailbox . '@' . $bcc->host)] = (isset($bcc->personal) AND !empty($bcc->personal)) ? $this->decodeMimeStr($bcc->personal, $this->serverEncoding) : null;
$header->bcc[strtolower($bcc->mailbox . '@' . $bcc->host)] = (isset($bcc->personal) AND !empty($bcc->personal)) ? $this->decodeMimeStr($bcc->personal, $this->getServerEncoding()) : null;
}
}
}

if(isset($head->reply_to)) {
foreach($head->reply_to as $replyTo) {
$header->replyTo[strtolower($replyTo->mailbox . '@' . $replyTo->host)] = (isset($replyTo->personal) AND !empty($replyTo->personal)) ? $this->decodeMimeStr($replyTo->personal, $this->serverEncoding) : null;
$header->replyTo[strtolower($replyTo->mailbox . '@' . $replyTo->host)] = (isset($replyTo->personal) AND !empty($replyTo->personal)) ? $this->decodeMimeStr($replyTo->personal, $this->getServerEncoding()) : null;
}
}

Expand All @@ -782,7 +808,7 @@ public function getMailHeader($mailId) {
* Get mail data
*
* @param $mailId
* @param bool $markAsSeen
* @param bool $markAsSeen Mark the email as seen, when set to true
* @return IncomingMail
*/
public function getMail($mailId, $markAsSeen = true) {
Expand Down Expand Up @@ -851,8 +877,8 @@ protected function initMailPart(IncomingMail $mail, $partStructure, $partNum, $m
}
else {
$fileName = !empty($params['filename']) ? $params['filename'] : $params['name'];
$fileName = $this->decodeMimeStr($fileName, $this->serverEncoding);
$fileName = $this->decodeRFC2231($fileName, $this->serverEncoding);
$fileName = $this->decodeMimeStr($fileName, $this->getServerEncoding());
$fileName = $this->decodeRFC2231($fileName, $this->getServerEncoding());
}

$attachment = new IncomingMailAttachment();
Expand Down Expand Up @@ -910,8 +936,8 @@ protected function initMailPart(IncomingMail $mail, $partStructure, $partNum, $m

/**
* Decodes a mime string
* @param string $string
* @param string $toEncoding
* @param string $string MIME string to decode
* @param string $toEncoding Charset, to which you want to decode it
* @return string Converted string if conversion was successful, or the original string if not
* @throws Exception
*/
Expand All @@ -923,7 +949,9 @@ public function decodeMimeStr($string, $toCharset = 'utf-8') {
$newString = '';
foreach(imap_mime_header_decode($string) as $element) {
if(isset($element->text)) {
$fromCharset = !isset($element->charset) || $element->charset == 'default' ? 'iso-8859-1' : $element->charset;
$fromCharset = !isset($element->charset) ? 'iso-8859-1' : $element->charset;
// Convert to UTF-8, if string has UTF-8 characters to avoid broken strings. See https://github.com/barbushin/php-imap/issues/232
$toCharset = isset($element->charset) && preg_match('/(UTF\-8)|(default)/i', $element->charset) ? 'UTF-8' : $toCharset;
$newString .= $this->convertStringEncoding($element->text, $fromCharset, $toCharset);
}
}
Expand All @@ -949,7 +977,7 @@ protected function decodeRFC2231($string, $charset = 'utf-8') {

/**
* Converts the datetime to a normalized datetime
* @param string header datetime
* @param string Header datetime
* @return datetime Normalized datetime
*/
public function parseDateTime($dateHeader) {
Expand All @@ -970,9 +998,9 @@ public function parseDateTime($dateHeader) {

/**
* Converts a string from one encoding to another.
* @param string $string
* @param string $fromEncoding
* @param string $toEncoding
* @param string $string The string, which you want to convert.
* @param string $fromEncoding The current charset (encoding).
* @param string $toEncoding The new charset (encoding).
* @return string Converted string if conversion was successful, or the original string if not
* @throws Exception
*/
Expand Down Expand Up @@ -1052,6 +1080,7 @@ public function getSubscribedMailboxes($search = "*") {
}

/**
* Subscribe to a mailbox
* @param $mailbox
* @throws Exception
*/
Expand All @@ -1060,6 +1089,7 @@ public function subscribeMailbox($mailbox) {
}

/**
* Unsubscribe from a mailbox
* @param $mailbox
* @throws Exception
*/
Expand All @@ -1070,10 +1100,10 @@ public function unsubscribeMailbox($mailbox) {
/**
* Call IMAP extension function call wrapped with utf7 args conversion & errors handling
*
* @param $methodShortName
* @param array|string $args
* @param bool $prependConnectionAsFirstArg
* @param string|null $throwExceptionClass
* @param string $methodShortName Name of PHP imap_ method, but without the 'imap_' prefix. (eg. imap_fetch_overview => fetch_overview)
* @param array|string $args All arguments of the original method, except the 'resource $imap_stream' (eg. imap_fetch_overview => string $sequence [, int $options = 0 ])
* @param bool $prependConnectionAsFirstArg Add 'resource $imap_stream' as first argument, if set to true
* @param string|null $throwExceptionClass Name of exception class, which will be thrown in case of errors
* @return mixed
* @throws Exception
*/
Expand Down
Loading

0 comments on commit ab18be7

Please sign in to comment.