Какие варианты решения Отследить предупреждения и исправить Есть предупреждение приходят иногда [24-Oct-2025 09:12:11 Etc/GMT-3] PHP Notice: iconv(): Detected an incomplete multibyte character in input string in I:\Inetpub\promo-ltd.ru\control.traceline\imap\lib\class.imap.php on line 416 [24-Oct-2025 09:12:11 Etc/GMT-3] PHP Notice: iconv(): Detected an illegal character in input string in I:\Inetpub\promo-ltd.ru\control.traceline\imap\lib\class.imap.php on line 416 [24-Oct-2025 09:12:11 Etc/GMT-3] PHP Notice: iconv(): Detected an incomplete multibyte character in input string in I:\Inetpub\promo-ltd.ru\control.traceline\imap\lib\class.imap.php on line 416 [24-Oct-2025 09:12:11 Etc/GMT-3] PHP Notice: iconv(): Detected an illegal character in input string in I:\Inetpub\promo-ltd.ru\control.traceline\imap\lib\class.imap.php on line 416 приходит файл с именем того вида _utf-8_B_0JPQsNGA0LDQvdGC0LjQudC90L7QtSDQotGA0LXQudGB0..... сам класс <?php /* * This class can be used to retrieve messages from an IMAP, POP3 and NNTP server * @author Kiril Kirkov * GitHub: https://github.com/kirilkirkov * Usage example: 1. $imap = new Imap(); 2. $connection_result = $imap->connect('{imap.gmail.com:993/imap/ssl}INBOX', 'user@gmail.com', 'secret_password'); if ($connection_result !== true) { echo $connection_result; //Error message! exit; } 3. $messages = $imap->getMessages('text'); //Array of messages * in $attachments_dir property set directory for attachments * in the __destructor set errors log */ class Imap { private $imapStream; private $plaintextMessage; private $htmlMessage; private $emails; private $errors = array(); private $attachments = array(); private $attachments_dir = 'attachments';//ERROR public function connect($hostname, $username, $password) { $connection = imap_open($hostname, $username, $password) or die('Cannot connect to Mail: ' . imap_last_error()); $this->imapStream = $connection; return true; } public function getMessages($type = 'text', $uid = null, $setcont='ALL') { $this->attachments_dir = rtrim($this->attachments_dir, '/'); $stream = $this->imapStream; $emails = imap_search($stream, $setcont);//'ALL' $messages = array(); if ($emails) { $this->emails = $emails; $this->attachments = array(); if(is_null($uid)) $uid = $_GET['id']; $messages[] = $this->loadMessage($uid, $type); } return array( "status" => "success", "data" => array_reverse($messages) ); } /* * Give message_numer from * returned array from - getMessages * if you want to delete message by UID, not Message number * set FT_UID to $uid. * Example $imap->deleteMessage(221, FT_UID); - 221 is uid */ public function deleteMessage($messageId, $uid = 0) { imap_delete($this->imapStream, $messageId, $uid); } public function getFiles($r) { //save attachments to directory /* $r['file'] = str_replace("UTF-8''",'',$r['file']); $r['file'] = urldecode($r['file']); $r['file'] = preg_replace("/[^A-Za-zА-Яа-я0-9-_.]+/u", "_", $r['file']);*/ $this->attachments_dir = $r['attachments_dir']; $pullPath = $this->attachments_dir . '/' . $r['path'] .'/'. $r['file']; //file_put_contents('_!!!_DEBUG-86nt8.txt',var_export($pullPath,1),FILE_APPEND); $res = true; if (file_exists($pullPath)) { $res = false; } elseif (!is_dir($this->attachments_dir)) { $this->errors[] = 'Cant find directory for email attachments! Message ID:' . $r['uid']; return false; } elseif (!is_writable($this->attachments_dir)) { $this->errors[] = 'Attachments directory is not writable! Message ID:' . $r['uid']; return false; } if (!is_dir($this->attachments_dir . '/' . $r['path'] .'/')) { mkdir($this->attachments_dir . '/' . $r['path'] .'/');//mkdir($dirname, 0755, true); } if($res && !preg_match('/\.php/i', $r['file']) && !preg_match('/\.cgi/i', $r['file']) && !preg_match('/\.exe/i', $r['file']) && !preg_match('/\.dll/i', $r['file']) && !preg_match('/\.mobileconfig/i', $r['file'])){ //fopen($pullPath, 'w');//!!!!!!!!!!!!!!!!!!!!!!!!! if (($filePointer = fopen($pullPath, 'w')) == false) { $this->errors[] = 'Cant open file at imap class to save attachment file! Message ID:' . $r['uid']; return false; } switch ($r['encoding']) { case 3: //base64 $streamFilter = stream_filter_append($filePointer, 'convert.base64-decode', STREAM_FILTER_WRITE); break; case 4: //quoted-printable $streamFilter = stream_filter_append($filePointer, 'convert.quoted-printable-decode', STREAM_FILTER_WRITE); break; default: $streamFilter = null; } imap_savebody($this->imapStream, $filePointer, $r['uid'], $r['part'], FT_UID); if ($streamFilter) { stream_filter_remove($streamFilter); } fclose($filePointer); return array("status" => "success", "path" => $r['path'], 'file'=>$r['file']);//return array("status" => "success", "path" => $pullPath); }else{ return array("status" => "success", "path" => $r['path'], 'file'=>$r['file']);//return array("status" => "success", "path" => $pullPath); } } private function loadMessage($uid, $type) { $overview = $this->getOverview($uid); $array = array(); $array['uid'] = $overview->uid; $array['subject'] = isset($overview->subject) ? $this->decode($overview->subject) : ''; $array['date'] = date('Y-m-d h:i:sa', strtotime($overview->date)); $headers = $this->getHeaders($uid); $array['from'] = isset($headers->from) ? $this->processAddressObject($headers->from) : array(''); $array['cc'] = isset($headers->cc) ? $this->processAddressObject($headers->cc) : array(''); $array['references'] = isset($overview->references) ? $overview->references : 0; $structure = $this->getStructure($uid); if (!isset($structure->parts)) { // not multipart $this->processStructure($uid, $structure); } else { // multipart foreach ($structure->parts as $id => $part) { $this->processStructure($uid, $part, $id + 1); } } // После обработки всех частей, сохраняем mapping CID → файл //$array['cid_mapping'] = array_column($this->attachments, 'filename_cid', 'cid'); $array['cid_mapping'] = array_column($this->attachments, 'file', 'cid'); // После обработки всех частей: if ($type === 'html' && !empty($this->htmlMessage)) { // Создаем маппинг CID → файлы $cidMapping = []; foreach ($this->attachments as $attachment) { if (!empty($attachment['cid'])) { $cidMapping[$attachment['cid']] = [ 'file' => $attachment['file'] = preg_replace("/[^A-Za-zА-Яа-я0-9-_.]+/u", "_", $attachment['file']), 'path' => date('Y-m-d'), // или другой путь из saveToDirectory 'cidi' => $array['cid_mapping'] ]; } } // Обрабатываем HTML $this->htmlMessage = $this->processCIDImages($this->htmlMessage, $cidMapping); } $array['message'] = $type == 'text' ? $this->plaintextMessage : $this->htmlMessage;// $email->getMessages('html',$value->uid) ..$tab->message $array['attachments'] = $this->attachments; return $array; } private function processStructure($uid, $structure, $partIdentifier = null) { $parameters = $this->getParametersFromStructure($structure); if ((isset($parameters['name']) || (isset($parameters['name*']) || isset($parameters['filename'])) || isset($parameters['filename*'])) || (isset($structure->subtype) && strtolower($structure->subtype) == 'rfc822') ) { if (isset($parameters['filename'])) { $this->setFileName($parameters['filename']); } elseif (isset($parameters['name'])) { $this->setFileName($parameters['name']); } elseif (isset($parameters['filename*'])) { $this->setFileName($parameters['filename*']); } elseif (isset($parameters['name*'])) { $this->setFileName($parameters['name*']); } // Добавляем обработку Content-ID Извлекаем CID (удаляем угловые скобки если есть) $contentId = isset($parameters['id']) ? trim($parameters['id'], '<>') : null; //$filename = $this->getAttachmentFilename($parameters); //$this->setFileName($filename); $this->encoding = $structure->encoding; $result_save = $this->saveToDirectory($uid, $partIdentifier); // Добавляем Content-ID в информацию о вложении $result_save['cid'] = $contentId; $this->attachments[] = $result_save; } elseif ($structure->type == 0 || $structure->type == 1) { $messageBody = isset($partIdentifier) ? imap_fetchbody($this->imapStream, $uid, $partIdentifier, FT_UID | FT_PEEK) : imap_body($this->imapStream, $uid, FT_UID | FT_PEEK); //file_put_contents('../_!!!_DEBUG-imap_fetchbody-index.txt',var_export($messageBody,1),FILE_APPEND); $messageBody = $this->decodeMessage($messageBody, $structure->encoding); //file_put_contents('../_!!!_DEBUG-decodeMessage-index.txt',var_export($messageBody,1),FILE_APPEND); /*$body = imap_qprint($messageBody); file_put_contents('../_!!!_DEBUG-FINALmessageBody1-index.txt',var_export($body,1),FILE_APPEND); $messageBodyTEST0 = imap_utf8($body); file_put_contents('../_!!!_DEBUG-FINALmessageBody2-index.txt',var_export($messageBodyTEST0,1),FILE_APPEND); $messageBodyTEST1 = iconv($parameters['charset'], 'UTF-8//TRANSLIT', $messageBody); file_put_contents('../_!!!_DEBUG-FINALmessageBody3-index.txt',var_export($messageBodyTEST1,1),FILE_APPEND); $messageBodyTEST2 = mb_convert_encoding($messageBody, 'UTF-8', $parameters['charset']); file_put_contents('../_!!!_DEBUG-FINALmessageBody4-index.txt',var_export($messageBodyTEST2,1),FILE_APPEND);*/ // Передаем HTML-контент для проверки метатегов $charset = $this->detectCharset($messageBody, $messageBody);//TEST 28.05.25 // Принудительная проверка русских кодировок //$charset = $this->detectRussianEncoding($messageBody) ?: $this->detectCharset($messageBody, $messageBody); if (mb_check_encoding($messageBody, $charset)) { if(!is_null($charset)) $messageBody = mb_convert_encoding($messageBody, 'UTF-8', $charset); //file_put_contents('../_!!!_DEBUG-FINALmessageBody5-index.txt',var_export($messageBody,1),FILE_APPEND); } $parameters__charset=''; if(!empty($parameters['charset'])) $parameters__charset=$parameters['charset']; file_put_contents(__DIR__ .'/../../crm/__!!!_DEBUG-encodingUIDdetectCharset-index.txt',var_export( ['uid'=>$uid, 'encoding'=>$structure->encoding, 'detectCharset'=>$this->detectCharset($messageBody), 'parameterscharset'=>$parameters__charset,'charsetHTMLtag'=>$charset,'structure'=>$structure ],1),FILE_APPEND); if (!empty($parameters['charset']) && strtolower($parameters['charset']) !== 'UTF-8') { if (function_exists('mb_convert_encoding')) { if (!in_array($parameters['charset'], mb_list_encodings())) {//нет кодировки if ($structure->encoding === 0) { $parameters['charset'] = 'US-ASCII'; } /*else { if ($structure->encoding === 4) { //$parameters['charset'] = 'koi8-r'; $parameters['charset'] = $this->detectCharset($messageBody);//TEST 25.05.25 } if($parameters['charset']!='windows-1251' && $parameters['charset'] != 'koi8-r') $parameters['charset'] = 'UTF-8'; //$parameters['charset'] = $this->detectCharset($messageBody);//TEST 25.05.25 else{ //$body = imap_qprint($messageBody); //$messageBody = imap_utf8($body); //file_put_contents('../_!!!_DEBUG-encoding1111IMAP-index.txt',var_export($body,1),FILE_APPEND); //file_put_contents('../_!!!_DEBUG-encoding222IMAP-index.txt',var_export($messageBody,1),FILE_APPEND); $parameters['charset'] = $this->detectCharset($messageBody);//TEST 25.05.25 } //file_put_contents('../_!!!_DEBUG-parametersIMAP-index.txt',var_export($parameters['charset'],1),FILE_APPEND);//if($parameters['charset']=='koi8-r') //file_put_contents('../_!!!_DEBUG-encodingIMAP-index.txt',var_export($structure->encoding,1),FILE_APPEND);//if($parameters['charset']=='koi8-r') }*/ if($parameters['charset'] !== 'GBK' && $parameters['charset']!='windows-1251' && $parameters['charset'] != 'koi8-r')//03.06.25 добавил && $parameters['charset'] != 'koi8-r' $parameters['charset'] = $this->detectCharset($messageBody);// $parameters['charset'] = 'UTF-8'; } //file_put_contents('../_!!!_DEBUG-encodingIMAP11111111111111-index.txt',var_export($parameters['charset'],1),FILE_APPEND);//if($parameters['charset']=='koi8-r') if(is_null($charset))//if($parameters['charset']!='windows-1251' && $charset != 'Windows-1251') $messageBody = mb_convert_encoding($messageBody, 'UTF-8', $parameters['charset']); /*else $messageBody = iconv($parameters['charset'], 'UTF-8//TRANSLIT', $messageBody);*/ } else { $messageBody = iconv($parameters['charset'], 'UTF-8//TRANSLIT', $messageBody); } } if (strtolower($structure->subtype) === 'plain' || ($structure->type == 1 && strtolower($structure->subtype) !== 'alternative')) { $this->plaintextMessage = ''; $this->plaintextMessage .= trim(htmlentities($messageBody)); $this->plaintextMessage = nl2br($this->plaintextMessage); } elseif (strtolower($structure->subtype) === 'html') { $this->htmlMessage = ''; $this->htmlMessage .= $messageBody; } //file_put_contents('../_!!!_DEBUG-FINALmessageBody-index.txt',var_export($messageBody,1),FILE_APPEND); } if (isset($structure->parts)) { foreach ($structure->parts as $partIndex => $part) { $partId = $partIndex + 1; if (isset($partIdentifier)) $partId = $partIdentifier . '.' . $partId; $this->processStructure($uid, $part, $partId); } } } private function processCIDImages($html, $cidMapping) { foreach ($cidMapping as $cid => $fileInfo) { if (!empty($cid)) { // Формируем относительный путь к файлу //$filePath = $this->attachments_dir . '/' . $fileInfo['path'] . '/' . $fileInfo['file'];// /*$html = str_replace( 'cid:' . $cid, $this->attachments_dir . '/' . date('Y-m-d') . '/' . $fileInfo, //$fileInfo['file'] filename $html );*/ // Заменяем все вхождения CID /*$html = str_replace( 'src="cid:' . $cid . '"', 'src="' . $filePath . '"', $html );*/ $html = str_replace( 'cid:' . $cid, 'cid:'.$fileInfo['file'].'@'.$cid, $html ); } } //file_put_contents('../_!!!_DEBUG-cidMapping.txt',var_export($cidMapping,1),FILE_APPEND); return $html; } private function isAttachment($structure, $parameters) { // Проверяем по наличию имени файла или специальному subtype return (isset($parameters['name']) || isset($parameters['filename']) || (isset($structure->subtype) && strtolower($structure->subtype) == 'rfc822')); } private function getAttachmentFilename($parameters) { if (isset($parameters['filename*'])) { return $this->decode($parameters['filename*']); } elseif (isset($parameters['name*'])) { return $this->decode($parameters['name*']); } elseif (isset($parameters['filename'])) { return $parameters['filename']; } elseif (isset($parameters['name'])) { return $parameters['name']; } return uniqid() . '.dat'; // fallback } private function setFileName($text) { $this->filename = $this->decode($text); } /* * save attachments to directory */ private function saveToDirectory($uid, $partIdentifier) { //save attachments to directory $array = array(); $array['part'] = $partIdentifier; if(empty($this->filename)) $array['file'] = 'noname12.eml'; else $array['file'] = $this->filename; $array['encoding'] = $this->encoding; // Генерируем уникальное имя файла, если нужно /*$fileExt = pathinfo($this->filename, PATHINFO_EXTENSION); $uniqueName = uniqid() . ($fileExt ? '.' . $fileExt : ''); $array['filename_cid'] = $uniqueName;*/ //$array['filename_cid'] = $this->filename; // Сохраняем файл /*$r = [ 'uid' => $uid, 'part' => $partIdentifier, 'file' => $uniqueName, 'path' => date('Y-m-d'), 'encoding' => $this->encoding ];*/ //$this->getFiles($r);//path не известен ? return $array; } private function decodeMessage($data, $encoding) { if (!is_numeric($encoding)) { $encoding = strtolower($encoding); } switch (true) { # 8BIT case $encoding === 1: return quoted_printable_decode(imap_8bit($data)); # BINARY case $encoding === 2: return imap_binary($data); # BASE64 case $encoding === 'base64': case $encoding === 3: return base64_decode($data); # QUOTED-PRINTABLE case $encoding === 'quoted-printable': case $encoding === 4: return quoted_printable_decode($data); default: return $data; } } /*private function decodeMessage($data, $encoding) { // Декодируем содержимое $decoded = parent::decodeMessage($data, $encoding); // Дополнительная обработка кодировки if (!mb_check_encoding($decoded, 'UTF-8')) { $charset = $this->detectCharset($decoded); $decoded = mb_convert_encoding($decoded, 'UTF-8', $charset); } // Чистка невидимых символов return preg_replace('/[^\P{C}\n\r\t]+/u', '', $decoded); }*/ private function getParametersFromStructure($structure) { $parameters = array(); // Основные параметры if (isset($structure->parameters)) { foreach ($structure->parameters as $param) { $parameters[strtolower($param->attribute)] = $param->value; } } // Дополнительные параметры (для RFC 2231) if (isset($structure->dparameters)) { foreach ($structure->dparameters as $param) { $parameters[strtolower($param->attribute)] = $param->value; } } // Специально для Content-ID if (isset($structure->id)) { $parameters['id'] = $structure->id; } return $parameters; } private function getOverview($uid) { $results = imap_fetch_overview($this->imapStream, $uid, FT_UID); $messageOverview = array_shift($results); if (!isset($messageOverview->date)) { $messageOverview->date = null; } return $messageOverview; } private function decode($text) { if (null === $text) { return null;//что то сделать '' типо нет текста } $result = ''; foreach (imap_mime_header_decode($text) as $word) { $ch = 'default' === $word->charset ? 'ascii' : $word->charset; $result .= iconv($ch, 'utf-8', $word->text); //текстировать бывает ошибка //PHP Notice: iconv(): Detected an incomplete multibyte character in input string in line 237 //PHP Notice: iconv(): Detected an illegal character in input string in line 237 /* $encoding = mb_detect_encoding($text, mb_detect_order(), false); if($encoding == "UTF-8") { $text = mb_convert_encoding($text, 'UTF-8', 'UTF-8'); } $out = iconv(mb_detect_encoding($text, mb_detect_order(), false), "UTF-8//IGNORE", $text);*/ } if($result=='') $result = $this->fallbackDecode($text);//'error5g9kd4uniqid'.date('Y-m-d'); return $result; } private function decodeHeader($text) { if (empty($text)) { return ''; } // Обработка MIME-заголовков (RFC 2047) if (preg_match('/=\?([^?]+)\?(Q|B)\?([^?]+)\?=/i', $text)) { $decoded = iconv_mime_decode($text, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, 'UTF-8'); return $decoded !== false ? $decoded : $this->fallbackDecode($text); } return $this->fallbackDecode($text); } private function fallbackDecode($text) { // Попробуем определить кодировку $encoding = mb_detect_encoding($text, [ 'UTF-8', 'Windows-1251', 'KOI8-R', 'ISO-8859-1', 'ISO-8859-5' ], true); if ($encoding === false) { $encoding = 'auto';// 'Windows-1251'; // Дефолтная для русских писем } // Конвертируем в UTF-8 с обработкой ошибок return mb_convert_encoding($text, 'UTF-8', $encoding); } /*private function sanitizeFilename($filename) { // Декодируем заголовок $cleanName = $this->decodeHeader($filename); // Удаляем опасные символы $cleanName = preg_replace([ '/[^\p{L}\p{N}\s\-_\.]/u', // Разрешаем буквы, цифры, пробелы, -_. '/\s+/', // Множественные пробелы '/\.\.+/', // Множественные точки '/^-+/', // Дефисы в начале '/-+$/' // Дефисы в конце ], ['', ' ', '.', '', ''], $cleanName); // Обрезаем длинное имя $cleanName = mb_substr($cleanName, 0, 100); // Если после обработки имя пустое - генерируем if (empty($cleanName)) { return uniqid('file_') . '.dat'; } return $cleanName; } private function getAttachmentFilename($parameters) { $filename = ''; // Приоритеты получения имени файла $sources = [ $parameters['filename*'] ?? null, $parameters['name*'] ?? null, $parameters['filename'] ?? null, $parameters['name'] ?? null ]; foreach ($sources as $source) { if (!empty($source)) { $filename = $this->sanitizeFilename($source); break; } } // Если имя не найдено в параметрах if (empty($filename)) { $ext = $this->getFileExtension($parameters); return uniqid('file_') . ($ext ? '.' . $ext : ''); } return $filename; } private function getFileExtension($parameters) { // Определяем по Content-Type if (isset($parameters['type'])) { $mime = strtolower($parameters['type']); $map = [ 'image/jpeg' => 'jpg', 'image/png' => 'png', 'application/pdf' => 'pdf' // ... дополните по необходимости ]; return $map[$mime] ?? null; } return null; }*/ /*private function normalizeCharset($charset) { //$charset = strtolower(trim($charset)); // Унифицируем распространённые варианты написания $aliases = [ 'koi8-r' => 'KOI8-R', 'koi8r' => 'KOI8-R', 'koi8u' => 'KOI8-U', 'windows-1251' => 'Windows-1251', 'win-1251' => 'Windows-1251', 'cp1251' => 'Windows-1251', 'iso-8859-5' => 'ISO-8859-5', 'iso8859-5' => 'ISO-8859-5', 'utf-8' => 'UTF-8', 'utf8' => 'UTF-8', 'shift_jis' => 'Shift_JIS', 'euc-jp' => 'EUC-JP', 'gb2312' => 'GB2312', 'big5' => 'BIG-5' ]; return $aliases[$charset] ?? strtoupper($charset); }*/ private function normalizeCharsetEncodings($charset) { //$charset = strtolower(trim($charset)); $extendedAliases = [ 'ibm866' => 'IBM866', 'cp866' => 'IBM866', 'dos-866' => 'IBM866' // ... остальные алиасы ]; // Специальные случаи, которые не совпадают с mb_list_encodings() $specialCases = [ 'koi8-r' => 'KOI8-R', 'koi8r' => 'KOI8-R', 'koi8u' => 'KOI8-U', 'windows-1251' => 'Windows-1251', 'win-1251' => 'Windows-1251', 'cp1251' => 'Windows-1251', 'iso-8859-5' => 'ISO-8859-5', 'iso8859-5' => 'ISO-8859-5', 'utf-8' => 'UTF-8', 'utf8' => 'UTF-8', 'shift_jis' => 'Shift_JIS', 'euc-jp' => 'EUC-JP', 'gb2312' => 'GB2312', 'big5' => 'BIG-5' ]; // Проверяем специальные случаи if (isset($specialCases[$charset])) { //return $specialCases[$charset] ?? strtoupper($charset); return $specialCases[$charset]; } // Получаем все поддерживаемые кодировки //$supportedEncodings = array_map('strtolower', mb_list_encodings()); // Ищем совпадение среди стандартных кодировок foreach (mb_list_encodings() as $enc) { if (strtolower($enc) === $charset) { return $enc; // Возвращаем в оригинальном регистре из mb_list_encodings() } } return null; // Неизвестная кодировка } private function isValidCharset($charset) { // Полный список поддерживаемых кодировок $supported = [ 'UTF-8', 'Windows-1251', 'KOI8-R', 'KOI8-U', 'ISO-8859-5', 'ISO-8859-1', 'Shift_JIS', 'EUC-JP', 'GB2312', 'BIG-5', 'Windows-1252', 'ISO-8859-2', 'ISO-8859-15' ]; return in_array($charset, $supported, true); } private function detectCharsetFromMeta($htmlContent) { if (preg_match('/<meta[^>]+charset=["\']?([^"\'\s>]+)/i', $htmlContent, $matches)) { $rawCharset = strtolower(trim($matches[1]));//trim($matches[1]); //$normalized = $this->normalizeCharset($rawCharset); $normalized = $this->normalizeCharsetEncodings($rawCharset); //какой то определенный стандарт кодировок! /*if ($this->isValidCharset($normalized)) { return $normalized; // Возвращаем в корректном регистре }*/ if ($normalized !== null) { // Лучше исключить эти псевдо-кодировки: $excluded = ['BASE64', 'UUENCODE', 'HTML-ENTITIES', 'Quoted-Printable']; //if ($normalized && in_array($normalized, mb_list_encodings())) return $normalized; } } return null; } /*private function detectCharset($text, $htmlContent = '') { // 1. Проверяем HTML-метатег если есть контент if (!empty($htmlContent) && preg_match('/<meta[^>]+charset=["\']?([^"\'\s>]+)/i', $htmlContent, $matches)) { $metaCharset = strtolower(trim($matches[1])); if (in_array($metaCharset, ['koi8-r', 'windows-1251', 'iso-8859-5'])) { return $metaCharset; } } // Автоопределение с приоритетом русских кодировок $charset = mb_detect_encoding($text, [ 'UTF-8', 'Windows-1251', 'KOI8-R', 'ISO-8859-5', 'ISO-8859-1', 'ASCII' ], true); return $charset ?: 'auto';//'Windows-1251'; }*/ private function detectCharset($text, $htmlContent = '') { // 1. Пробуем определить из метатега if (!empty($htmlContent)) { $metaCharset = $this->detectCharsetFromMeta($htmlContent); /*if ($metaCharset !== null) { return $metaCharset; // Уже в правильном регистре }*/ return $metaCharset; } // TEXT !!!!!!!! Проверка русских кодировок в первую очередь /*if ($russianEnc = $this->detectRussianEncoding($text)) { return $russianEnc; }*/ // 2. Автоопределение (существующая логика) //return $this->strictDetectEncoding($text); //НЕ ПРОБОВАЛ! По идеи лучше должно быть /*if ($exotic = $this->detectExoticEncoding($text)) { return $exotic; }*/ // Автоопределение с приоритетом русских кодировок $charset = mb_detect_encoding($text, [ 'UTF-8', 'Windows-1251', 'KOI8-R', 'ISO-8859-5', 'ISO-8859-1' ], true); return $charset ?: 'Windows-1251';//'auto';//'Windows-1251'; } private function isValidEncoding($text, $encoding) { // Конвертируем туда-обратно и сравниваем $converted = mb_convert_encoding( mb_convert_encoding($text, 'UTF-8', $encoding), $encoding, 'UTF-8' ); return $text === $converted; } private function strictDetectEncoding($text) { // Сначала проверяем русские кодировки if ($russianEnc = $this->detectRussianEncoding($text)) { return $russianEnc; } $encodings = ['UTF-8', 'Windows-1251', 'KOI8-R', 'ISO-8859-5', 'ISO-8859-1']; foreach ($encodings as $enc) { if ($this->isValidEncoding($text, $enc)) { return $enc; } } // Fallback с проверкой русских символов /*if (preg_match('/[\x80-\xFF]/', $text)) { return 'Windows-1251'; }*/ return 'UTF-8';//или лучше auto !!!!!! Пробовать тестить нужно } private function detectExoticEncoding($text) { // Специфические паттерны для редких кодировок $patterns = [ '/\xFD[\x80-\xFF]/' => 'IBM866', // Русская DOS-кодировка '/\xA3[\x80-\xFF]/' => 'KOI8-U', // Украинская KOI8 '/\xB3[\x80-\xFF]/' => 'ISO-8859-5' // Альтернативная кириллица ]; foreach ($patterns as $pattern => $encoding) { if (preg_match($pattern, $text)) { return $encoding; } } // Проверка через статистику символов $stats = count_chars($text, 1); $ranges = [ 'Windows-1251' => [192, 255], 'KOI8-R' => [224, 255], 'IBM866' => [128, 175] ]; foreach ($ranges as $enc => $range) { $matches = 0; foreach ($stats as $ord => $count) { if ($ord >= $range[0] && $ord <= $range[1]) { $matches += $count; } } if ($matches / strlen($text) > 0.3) { // 30% символов в диапазоне return $enc; } } return null; } private function detectRussianEncoding($text) { // Специальная проверка для повторяющихся Ї if (preg_match('/\xD2[\x80-\xFF]{5,}/', $text)) { // Ї в Windows-1251 return 'Windows-1251'; } // Fallback с проверкой русских символов if (preg_match('/[\x80-\xFF]/', $text)) { return 'Windows-1251'; } // Статистика символов для русских кодировок $ranges = [ 'Windows-1251' => [192, 255], // Основной диапазон кириллицы 'KOI8-R' => [224, 255], // KOI8-R специфичный диапазон 'IBM866' => [128, 175] // DOS-кодировка ]; $counts = []; foreach ($ranges as $enc => $range) { $counts[$enc] = 0; } // Анализируем каждый символ $length = strlen($text); for ($i = 0; $i < $length; $i++) { $ord = ord($text[$i]); foreach ($ranges as $enc => $range) { if ($ord >= $range[0] && $ord <= $range[1]) { $counts[$enc]++; } } } // Выбираем кодировку с максимальным совпадением arsort($counts); $bestMatch = key($counts); // Если найдено достаточное количество русских символов if ($counts[$bestMatch] > 0 && $counts[$bestMatch] / $length > 0.3) { return $bestMatch; } return null; } private function processAddressObject($addresses) { $outputAddresses = array(); if (is_array($addresses)) foreach ($addresses as $address) { if (property_exists($address, 'mailbox') && $address->mailbox != 'undisclosed-recipients') { $currentAddress = array(); $currentAddress['address'] = $address->mailbox . '@' . $address->host; if (isset($address->personal)) { $currentAddress['name'] = $this->decode($address->personal); } $outputAddresses = $currentAddress; } } return $outputAddresses; } private function getHeaders($uid) { $rawHeaders = $this->getRawHeaders($uid); $headerObject = imap_rfc822_parse_headers($rawHeaders); if (isset($headerObject->date)) { $headerObject->udate = strtotime($headerObject->date); } else { $headerObject->date = null; $headerObject->udate = null; } $this->headers = $headerObject; return $this->headers; } private function getRawHeaders($uid) { $rawHeaders = imap_fetchheader($this->imapStream, $uid, FT_UID); return $rawHeaders; } private function getStructure($uid) { $structure = imap_fetchstructure($this->imapStream, $uid, FT_UID); return $structure; } public function __destruct() { if (!empty($this->errors)) { foreach ($this->errors as $error) { //SAVE YOUR LOG OF ERRORS } } } }

The issue you're encountering is related to the PHP iconv() function throwing notices about "incomplete multibyte character" or "illegal character" in the input string, specifically in the decode method of your Imap class (line 416). This occurs when processing email headers or content with problematic encodings, often due to non-standard or corrupted character sequences. Additionally, you mentioned that attachment filenames appear in a format like _utf-8_B_0JPQsNGA0LDQvdGC0LjQudC90L7QtSDQotGA0LXQudGB0..., indicating issues with MIME header decoding.

Below, I'll outline the root causes of these issues and provide actionable solutions to track and fix the warnings, as well as address the filename decoding problem.


Root Causes

  1. iconv() Notices:
    • The iconv() function in the decode method (line 416) fails when it encounters invalid or incomplete multibyte sequences in the input string (e.g., email subject or attachment names).
    • This is common when emails use non-standard encodings, corrupted data, or encodings not properly supported by iconv().
    • The decode method uses imap_mime_header_decode() to split the header into parts, but it doesn't handle cases where the charset is invalid or unsupported by iconv().
  2. Filename Decoding Issues:
    • The attachment filenames (e.g., _utf-8_B_0JPQsNGA0LDQvdGC0LjQudC90L7QtSDQotGA0LXQudGB0...) suggest that the MIME header decoding is failing, likely because the filename is encoded in a format like =?UTF-8?B?...?=, but the decoding logic isn't properly handling it.
    • The setFileName and saveToDirectory methods rely on the decode method, which may produce invalid or unreadable filenames if the input is malformed.
  3. Lack of Robust Error Handling:
    • The decode method attempts a fallback with fallbackDecode, but it may not always resolve the issue, especially for complex or mixed encodings.
    • The class doesn't log or handle these errors in a way that makes debugging straightforward.

Solutions

To address the iconv() notices and the filename decoding issues, we need to:

  • Improve the decode method to handle problematic encodings gracefully.
  • Enhance filename sanitization and decoding for attachments.
  • Add logging to track problematic inputs for debugging.
  • Test and validate the changes to ensure robustness.

Below are the specific steps and code modifications:

1. Improve the decode Method

The decode method is where the iconv() notices occur. We'll modify it to:

  • Validate the charset before passing it to iconv().
  • Use mb_convert_encoding() as a fallback if iconv() fails.
  • Log problematic inputs for debugging.

Here's the updated decode method:

php
private function decode($text) {
    if (empty($text)) {
        return '';
    }

    $result = '';
    $decoded_parts = imap_mime_header_decode($text);

    if ($decoded_parts === false) {
        // Log the error and fallback
        $this->errors[] = "Failed to decode MIME header: " . $text;
        return $this->fallbackDecode($text);
    }

    foreach ($decoded_parts as $word) {
        $charset = ($word->charset === 'default') ? 'ascii' : $word->charset;
        $normalizedCharset = $this->normalizeCharsetEncodings($charset);

        if ($normalizedCharset && in_array($normalizedCharset, mb_list_encodings())) {
            try {
                $converted = iconv($normalizedCharset, 'UTF-8//IGNORE', $word->text);
                if ($converted === false) {
                    throw new Exception("iconv failed for charset: $normalizedCharset");
                }
                $result .= $converted;
            } catch (Exception $e) {
                $this->errors[] = "iconv error for charset '$normalizedCharset' on text: " . $word->text;
                $result .= $this->fallbackDecode($word->text);
            }
        } else {
            // Invalid or unsupported charset, use fallback
            $this->errors[] = "Unsupported charset '$charset' in text: " . $word->text;
            $result .= $this->fallbackDecode($word->text);
        }
    }

    if (empty($result)) {
        $this->errors[] = "Empty result after decoding text: " . $text;
        $result = $this->fallbackDecode($text);
    }

    return $result;
}

Key Changes:

  • Added validation of the charset using normalizeCharsetEncodings and mb_list_encodings().
  • Wrapped iconv() in a try-catch block to catch errors and use //IGNORE to skip invalid characters.
  • Log errors to the $this->errors array for debugging.
  • Fallback to fallbackDecode if iconv() fails or the charset is invalid.

2. Enhance Filename Handling

The filenames like _utf-8_B_0JPQsNGA0LDQvdGC0LjQudC90L7QtSDQotGA0LXQudGB0... indicate that the MIME-encoded filename isn't being decoded properly. We'll improve the setFileName and saveToDirectory methods to handle this.

Update the setFileName method to sanitize and decode filenames properly:

php
private function setFileName($text) {
    $decoded = $this->decode($text);
    // Sanitize the filename to remove invalid characters
    $cleanName = preg_replace('/[^\p{L}\p{N}\s\-_\.]/u', '_', $decoded);
    $cleanName = preg_replace('/\s+/', '_', $cleanName); // Replace multiple spaces with underscore
    $cleanName = mb_substr($cleanName, 0, 100); // Limit length
    $this->filename = empty($cleanName) ? uniqid('file_') . '.dat' : $cleanName;
}

Update the saveToDirectory method to ensure proper filename handling:

php
private function saveToDirectory($uid, $partIdentifier) {
    $array = array();
    $array['part'] = $partIdentifier;
    $array['file'] = empty($this->filename) ? 'noname_' . uniqid() . '.eml' : $this->filename;
    $array['encoding'] = $this->encoding;
    $array['path'] = date('Y-m-d'); // Store in a date-based directory

    // Ensure the filename is URL-decoded and cleaned
    $array['file'] = urldecode($array['file']);
    $array['file'] = preg_replace('/[^\p{L}\p{N}\s\-_\.]/u', '_', $array['file']);

    return $array;
}

Key Changes:

  • Decode the filename using the improved decode method.
  • Sanitize the filename to remove invalid characters and limit length.
  • Use urldecode() to handle cases where the filename contains encoded characters (e.g., from MIME headers).
  • Fallback to a unique filename if decoding fails.

3. Improve getFiles Method for Attachment Saving

The getFiles method saves attachments to the filesystem. Ensure it uses the sanitized filename and logs any issues:

php
public function getFiles($r) {
    $this->attachments_dir = rtrim($r['attachments_dir'], '/');
    $pullPath = $this->attachments_dir . '/' . $r['path'] . '/' . $r['file'];

    // Log the attempt to save the file
    $this->errors[] = "Attempting to save attachment: $pullPath";

    if (file_exists($pullPath)) {
        $this->errors[] = "File already exists: $pullPath";
        return false;
    }

    if (!is_dir($this->attachments_dir)) {
        $this->errors[] = "Directory not found: $this->attachments_dir";
        return false;
    }

    if (!is_writable($this->attachments_dir)) {
        $this->errors[] = "Directory not writable: $this->attachments_dir";
        return false;
    }

    if (!is_dir($this->attachments_dir . '/' . $r['path'])) {
        if (!mkdir($this->attachments_dir . '/' . $r['path'], 0755, true)) {
            $this->errors[] = "Failed to create directory: " . $this->attachments_dir . '/' . $r['path'];
            return false;
        }
    }

    // Prevent saving dangerous file types
    if (preg_match('/\.(php|cgi|exe|dll|mobileconfig)$/i', $r['file'])) {
        $this->errors[] = "Blocked dangerous file type: " . $r['file'];
        return array("status" => "error", "message" => "Blocked file type", "file" => $r['file']);
    }

    $filePointer = fopen($pullPath, 'w');
    if ($filePointer === false) {
        $this->errors[] = "Failed to open file for writing: $pullPath";
        return false;
    }

    switch ($r['encoding']) {
        case 3: // base64
            $streamFilter = stream_filter_append($filePointer, 'convert.base64-decode', STREAM_FILTER_WRITE);
            break;
        case 4: // quoted-printable
            $streamFilter = stream_filter_append($filePointer, 'convert.quoted-printable-decode', STREAM_FILTER_WRITE);
            break;
        default:
            $streamFilter = null;
    }

    $success = imap_savebody($this->imapStream, $filePointer, $r['uid'], $r['part'], FT_UID);
    if ($streamFilter) {
        stream_filter_remove($streamFilter);
    }
    fclose($filePointer);

    if (!$success) {
        $this->errors[] = "Failed to save attachment: $pullPath";
        return array("status" => "error", "message" => "Failed to save attachment", "file" => $r['file']);
    }

    return array("status" => "success", "path" => $r['path'], "file" => $r['file']);
}

Key Changes:

  • Added detailed error logging for each step (directory creation, file writing, etc.).
  • Ensured the filename is sanitized and safe.
  • Improved error handling for file operations.

4. Enhance Error Logging

To track warnings effectively, ensure the __destruct method logs errors to a file or another logging system:

php
public function __destruct() {
    if (!empty($this->errors)) {
        $logFile = __DIR__ . '/../../logs/imap_errors.log';
        $logMessage = "[" . date('Y-m-d H:i:s') . "] IMAP Errors:\n";
        foreach ($this->errors as $error) {
            $logMessage .= $error . "\n";
        }
        file_put_contents($logFile, $logMessage, FILE_APPEND);
    }
    if ($this->imapStream) {
        imap_close($this->imapStream);
    }
}

Key Changes:

  • Save errors to a log file (imap_errors.log) for debugging.
  • Include timestamps and detailed error messages.
  • Ensure the IMAP connection is closed properly.

5. Test and Validate

  • Test with Problematic Emails: Use emails with known problematic encodings (e.g., Windows-1251, KOI8-R, or malformed headers) to verify that the notices are gone and filenames are decoded correctly.
  • Check Filenames: Ensure attachment filenames are readable and properly saved (e.g., no _utf-8_B_... prefixes).
  • Monitor Logs: Review the imap_errors.log file to identify any remaining issues with specific emails or charsets.

Additional Recommendations

  1. Update PHP and Extensions:
    • Ensure you're using a recent version of PHP (e.g., 7.4 or later) and that the iconv and mbstring extensions are properly configured.
    • Some older PHP versions have bugs in iconv() that can cause issues with certain encodings.
  2. Validate Input Data:
    • Add checks in the processStructure method to log the raw input data when decoding fails, so you can inspect problematic emails.
  3. Use a Library for MIME Parsing:
    • Consider using a library like Zend\Mail or PHP-IMAP for more robust MIME parsing and decoding, as they handle edge cases better than PHP's built-in imap functions.
  4. Handle Specific Encodings:
    • For Russian emails, prioritize encodings like Windows-1251 and KOI8-R in the detectCharset method, as shown in your existing detectRussianEncoding method.

Example Output

After applying these changes, attachment filenames should be properly decoded (e.g., document.pdf instead of _utf-8_B_...), and iconv() notices should be eliminated or logged for debugging. The log file (imap_errors.log) will contain entries like:

text
[2025-10-24 09:12:11] IMAP Errors:
Unsupported charset 'invalid-charset' in text: =?invalid-charset?B?...
Attempting to save attachment: attachments/2025-10-24/document.pdf

This allows you to track which emails are causing issues and why.


Testing the Fix

To verify the solution:

  1. Send test emails with Russian text, different encodings (e.g., Windows-1251, KOI8-R, UTF-8), and attachments with complex names.
  2. Check the attachments directory for correctly named files.
  3. Monitor the imap_errors.log file for any new errors.
  4. Ensure no PHP notices appear in the error log.

If you encounter specific cases where the issue persists, share the problematic email headers or content (sanitized if necessary), and I can help refine the solution further.

Let me know if you need additional clarification or assistance with testing!

11,1с
Быстро
How can Grok help?