mdb = $mdb; else { global $mdb; $this -> mdb = $mdb; } $this -> attachments = new TaskAttachmentRepository( $this -> mdb ); $this -> last_ai_error = ''; } public function importFromImap( array $config ) { if ( !extension_loaded( 'imap' ) ) return [ 'status' => 'error', 'msg' => 'Brak rozszerzenia IMAP w PHP.' ]; $mailbox = $this -> buildMailbox( $config ); $username = isset( $config['username'] ) ? (string)$config['username'] : ''; $password = isset( $config['password'] ) ? (string)$config['password'] : ''; if ( $mailbox === '' or $username === '' or $password === '' ) return [ 'status' => 'error', 'msg' => 'Brak konfiguracji skrzynki IMAP.' ]; $imap = @imap_open( $mailbox, $username, $password ); if ( !$imap ) return [ 'status' => 'error', 'msg' => 'Nie udalo sie polaczyc z IMAP: ' . imap_last_error() ]; $this -> ensureImportTable(); $message_numbers = imap_search( $imap, 'ALL' ); if ( !is_array( $message_numbers ) or !count( $message_numbers ) ) { imap_close( $imap ); return [ 'status' => 'empty', 'msg' => 'Brak wiadomosci do importu.' ]; } sort( $message_numbers, SORT_NUMERIC ); $imported = 0; $skipped = 0; $errors = 0; $first_error_msg = null; foreach ( $message_numbers as $message_no ) { $header = @imap_headerinfo( $imap, $message_no ); if ( !$header ) { $errors++; continue; } $sender = $this -> extractSender( $header ); $subject = $this -> decodeHeaderValue( isset( $header -> subject ) ? $header -> subject : '' ); $received_at = $this -> parseReceivedDate( isset( $header -> date ) ? $header -> date : '' ); $message_key = $this -> messageKey( $header, $sender, $subject, $message_no ); if ( $this -> isMessageFinalized( $message_key ) ) { $skipped++; @imap_delete( $imap, $message_no ); continue; } if ( $sender !== self::ALLOWED_SENDER ) { $this -> saveImportLog( $message_key, null, $sender, $subject, 'skipped_sender', null ); $skipped++; @imap_delete( $imap, $message_no ); continue; } try { $content = $this -> extractMessageContent( $imap, $message_no ); $ai_used = false; $ai_error_for_log = null; // Sprawdź czy użyć AI do parsowania global $settings; $use_ai = isset( $settings['openai_parse_emails'] ) && $settings['openai_parse_emails'] === true; $api_key = isset( $settings['openai_api_key'] ) ? trim( (string)$settings['openai_api_key'] ) : ''; if ( $use_ai && $api_key === '' ) { $ai_error_for_log = 'Brak klucza API OpenAI.'; error_log( '[MailToTaskImporter] AI fallback: ' . $ai_error_for_log ); } if ( $use_ai && $api_key !== '' ) { $model = isset( $settings['openai_model'] ) ? trim( (string)$settings['openai_model'] ) : 'gpt-4o-mini'; $ai_result = $this -> parseWithAI( $api_key, $model, $subject, $content['text'] ); if ( is_array( $ai_result ) && isset( $ai_result['task_name'] ) && isset( $ai_result['task_text'] ) ) { $task_name = $ai_result['task_name']; $task_text = $ai_result['task_text']; $ai_used = true; } else { // Fallback do normalnego parsowania jeśli AI nie zadziała if ( $this -> last_ai_error !== '' ) { error_log( '[MailToTaskImporter] AI fallback: ' . $this -> last_ai_error ); $ai_error_for_log = $this -> last_ai_error; } $task_name = trim( $subject ) !== '' ? trim( $subject ) : '(bez tematu)'; $task_text = $this -> prepareImportedTaskText( $content['text'] ); } } else { // Normalne parsowanie $task_name = trim( $subject ) !== '' ? trim( $subject ) : '(bez tematu)'; $task_text = $this -> prepareImportedTaskText( $content['text'] ); } $client_id = $this -> resolveClientIdBySenderDomain( $sender ); $task_id = \factory\Tasks::task_save( null, null, null, self::TASK_USER_ID, $task_name, $task_text, $received_at, $received_at, self::TASK_PROJECT_ID, $client_id, null, null, 'off', 0, 0, [ self::TASK_USER_ID ], null, null, false, 'off', true, self::TASK_STATUS_ID, 'on', 0 ); if ( !$task_id ) { $db_error = $this -> mdb -> error(); $db_error_text = is_array( $db_error ) ? implode( ' | ', array_filter( $db_error ) ) : ''; $error_msg = 'Nie udalo sie utworzyc zadania.' . ( $db_error_text !== '' ? ' DB: ' . $db_error_text : '' ); $this -> saveImportLog( $message_key, null, $sender, $subject, 'error', $error_msg ); if ( $first_error_msg === null ) $first_error_msg = $error_msg; $errors++; continue; } // Zapisz inline images jako załączniki i zbierz URLe $cid_to_url = []; if ( isset( $content['inline_images'] ) && is_array( $content['inline_images'] ) ) { foreach ( $content['inline_images'] as $cid => $inline_image ) { $img_name = trim( $inline_image['name'] ); if ( $img_name === '' ) $img_name = 'inline_' . md5( $cid ) . '.' . $this -> guessExtensionFromMime( $inline_image['mime'] ); $result = $this -> attachments -> uploadFromContent( $task_id, self::TASK_USER_ID, $img_name, $inline_image['content'] ); if ( is_array( $result ) && $result['status'] === 'success' ) $cid_to_url[$cid] = '/upload/task_attachments/' . $result['relative_path'] . '/' . $result['stored_name']; } } // Jeśli jest HTML, zastąp CIDy na URLe załączników i wyczyść style if ( isset( $content['html'] ) && trim( $content['html'] ) !== '' ) { $html_clean = $content['html']; if ( !empty( $cid_to_url ) ) $html_clean = $this -> replaceCidReferences( $html_clean, $cid_to_url ); $task_text = $this -> prepareImportedTaskTextFromHtml( $html_clean ); // Zaktualizuj treść zadania $this -> mdb -> update( 'tasks', [ 'text' => $task_text ], [ 'id' => $task_id ] ); } // Zapisz normalne załączniki foreach ( $content['attachments'] as $attachment ) { $att_name = self::normalizeImportedAttachmentName( isset( $attachment['name'] ) ? $attachment['name'] : '', isset( $attachment['mime'] ) ? $attachment['mime'] : '', isset( $attachment['content'] ) ? $attachment['content'] : null ); $att_mime = isset( $attachment['mime'] ) ? $attachment['mime'] : ''; // Jeśli nazwa nie ma rozszerzenia, dodaj je z MIME type if ( pathinfo( $att_name, PATHINFO_EXTENSION ) === '' && $att_mime !== '' ) { $mime_ext = $this -> extensionFromMime( $att_mime ); if ( $mime_ext !== '' ) $att_name .= '.' . $mime_ext; } $this -> attachments -> uploadFromContent( $task_id, self::TASK_USER_ID, $att_name, $attachment['content'] ); } $import_status = 'imported'; $import_error = null; if ( $use_ai && $ai_used ) $import_status = 'imported_ai'; elseif ( $use_ai && !$ai_used ) { $import_status = 'imported_fallback'; $import_error = $ai_error_for_log; } $this -> saveImportLog( $message_key, $task_id, $sender, $subject, $import_status, $import_error ); $imported++; } catch ( \Throwable $e ) { $this -> saveImportLog( $message_key, null, $sender, $subject, 'error', $e -> getMessage() ); if ( $first_error_msg === null ) $first_error_msg = $e -> getMessage(); $errors++; } $status_tmp = $this -> getImportStatus( $message_key ); if ( in_array( $status_tmp, [ 'imported', 'imported_ai', 'imported_fallback', 'skipped_sender' ], true ) ) @imap_delete( $imap, $message_no ); } @imap_expunge( $imap ); @imap_close( $imap ); if ( $imported > 0 ) return [ 'status' => 'ok', 'msg' => 'Zaimportowano: ' . $imported . ', pominieto: ' . $skipped . ', bledy: ' . $errors . '.' ]; if ( $errors > 0 ) { $msg = 'Brak nowych zadan. Bledy: ' . $errors . '.'; if ( $first_error_msg ) $msg .= ' Ostatni blad: ' . $first_error_msg; return [ 'status' => 'error', 'msg' => $msg ]; } return [ 'status' => 'empty', 'msg' => 'Brak nowych zadan. Pominieto: ' . $skipped . '.' ]; } private function buildMailbox( array $config ) { $mailbox = isset( $config['mailbox'] ) ? trim( (string)$config['mailbox'] ) : ''; if ( $mailbox !== '' ) return $mailbox; $host = isset( $config['host'] ) ? trim( (string)$config['host'] ) : ''; if ( $host === '' ) return ''; $port = isset( $config['port'] ) ? (int)$config['port'] : 993; $flags = isset( $config['flags'] ) ? trim( (string)$config['flags'] ) : '/imap/ssl'; $folder = isset( $config['folder'] ) ? trim( (string)$config['folder'] ) : 'INBOX'; return '{' . $host . ':' . $port . $flags . '}' . $folder; } private function resolveClientIdBySenderDomain( $sender ) { $domain = $this -> extractDomainFromEmail( $sender ); if ( $domain === '' ) return null; $rows = $this -> mdb -> select( 'crm_client', [ 'id', 'emails' ], [ 'emails[!]' => null ] ); if ( !is_array( $rows ) or !count( $rows ) ) return null; foreach ( $rows as $row ) { $emails = $this -> parseEmailsField( isset( $row['emails'] ) ? $row['emails'] : '' ); foreach ( $emails as $email ) { $email_domain = $this -> extractDomainFromEmail( $email ); if ( $email_domain !== '' and $email_domain === $domain ) return (int)$row['id']; } } return null; } private function parseEmailsField( $emails_raw ) { $emails_raw = (string)$emails_raw; if ( trim( $emails_raw ) === '' ) return []; preg_match_all( '/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i', $emails_raw, $matches ); if ( isset( $matches[0] ) and is_array( $matches[0] ) and count( $matches[0] ) ) return array_values( array_unique( array_map( 'strtolower', $matches[0] ) ) ); return []; } private function extractDomainFromEmail( $email ) { $email = strtolower( trim( (string)$email ) ); if ( $email === '' or strpos( $email, '@' ) === false ) return ''; $parts = explode( '@', $email ); $domain = trim( end( $parts ) ); return $domain !== '' ? $domain : ''; } private function parseReceivedDate( $value ) { $timestamp = strtotime( (string)$value ); if ( $timestamp === false ) return date( 'Y-m-d' ); return date( 'Y-m-d', $timestamp ); } private function extractSender( $header ) { if ( !isset( $header -> from ) or !is_array( $header -> from ) or !isset( $header -> from[0] ) ) return ''; $item = $header -> from[0]; $mailbox = isset( $item -> mailbox ) ? strtolower( trim( $item -> mailbox ) ) : ''; $host = isset( $item -> host ) ? strtolower( trim( $item -> host ) ) : ''; if ( $mailbox === '' or $host === '' ) return ''; return $mailbox . '@' . $host; } private function decodeHeaderValue( $value ) { $value = (string)$value; if ( $value === '' ) return ''; if ( !function_exists( 'imap_mime_header_decode' ) ) return trim( $this -> convertTextToUtf8( $value ) ); $chunks = @imap_mime_header_decode( $value ); if ( !is_array( $chunks ) or !count( $chunks ) ) return trim( $this -> convertTextToUtf8( $value ) ); $decoded = ''; foreach ( $chunks as $chunk ) { $chunk_text = isset( $chunk -> text ) ? (string)$chunk -> text : ''; $chunk_charset = isset( $chunk -> charset ) ? (string)$chunk -> charset : ''; $decoded .= $this -> convertTextToUtf8( $chunk_text, $chunk_charset ); } return trim( $decoded ); } private function convertTextToUtf8( $text, $source_charset = '' ) { $text = (string)$text; if ( $text === '' ) return ''; if ( preg_match( '//u', $text ) ) return $text; $source_charset = strtoupper( trim( (string)$source_charset ) ); $aliases = [ 'DEFAULT' => '', 'UNKNOWN-8BIT' => '', 'X-UNKNOWN' => '', 'UTF8' => 'UTF-8', 'UTF-8' => 'UTF-8', 'ISO-8859-2' => 'ISO-8859-2', 'ISO8859-2' => 'ISO-8859-2', 'WINDOWS-1250' => 'Windows-1250', 'CP1250' => 'Windows-1250', 'WINDOWS-1252' => 'Windows-1252', 'CP1252' => 'Windows-1252' ]; $candidates = []; if ( isset( $aliases[$source_charset] ) && $aliases[$source_charset] !== '' ) $candidates[] = $aliases[$source_charset]; $candidates = array_merge( $candidates, [ 'Windows-1250', 'ISO-8859-2', 'Windows-1252', 'ISO-8859-1' ] ); $candidates = array_values( array_unique( $candidates ) ); foreach ( $candidates as $encoding ) { if ( function_exists( 'mb_convert_encoding' ) ) { $converted = @mb_convert_encoding( $text, 'UTF-8', $encoding ); if ( is_string( $converted ) && $converted !== '' && preg_match( '//u', $converted ) ) return $converted; } if ( function_exists( 'iconv' ) ) { $converted = @iconv( $encoding, 'UTF-8//IGNORE', $text ); if ( is_string( $converted ) && $converted !== '' && preg_match( '//u', $converted ) ) return $converted; } } if ( function_exists( 'mb_convert_encoding' ) ) { $converted = @mb_convert_encoding( $text, 'UTF-8', 'UTF-8' ); if ( is_string( $converted ) && $converted !== '' ) return $converted; } return @iconv( 'UTF-8', 'UTF-8//IGNORE', $text ) ?: $text; } private function messageKey( $header, $sender, $subject, $message_no ) { $message_id = ''; if ( isset( $header -> message_id ) and trim( (string)$header -> message_id ) !== '' ) $message_id = trim( (string)$header -> message_id ); if ( $message_id === '' ) $message_id = sha1( $sender . '|' . $subject . '|' . ( isset( $header -> date ) ? $header -> date : '' ) . '|' . $message_no ); return $message_id; } private function isMessageFinalized( $message_key ) { $status = $this -> getImportStatus( $message_key ); return in_array( $status, [ 'imported', 'imported_ai', 'imported_fallback', 'skipped_sender' ], true ); } private function getImportStatus( $message_key ) { return $this -> mdb -> get( 'tasks_mail_import', 'status', [ 'message_key' => (string)$message_key ] ); } private function saveImportLog( $message_key, $task_id, $sender, $subject, $status, $error ) { $data = [ 'task_id' => $task_id ? (int)$task_id : null, 'sender' => substr( (string)$sender, 0, 255 ), 'subject' => substr( (string)$subject, 0, 255 ), 'status' => substr( (string)$status, 0, 40 ), 'error_msg' => $error !== null ? (string)$error : null, 'date_add' => date( 'Y-m-d H:i:s' ) ]; if ( $this -> mdb -> count( 'tasks_mail_import', [ 'message_key' => (string)$message_key ] ) ) $this -> mdb -> update( 'tasks_mail_import', $data, [ 'message_key' => (string)$message_key ] ); else $this -> mdb -> insert( 'tasks_mail_import', array_merge( [ 'message_key' => (string)$message_key ], $data ) ); } private function ensureImportTable() { $this -> mdb -> query( 'CREATE TABLE IF NOT EXISTS `tasks_mail_import` ( `id` INT NOT NULL AUTO_INCREMENT, `message_key` VARCHAR(255) NOT NULL, `task_id` INT NULL, `sender` VARCHAR(255) NULL, `subject` VARCHAR(255) NULL, `status` VARCHAR(40) NOT NULL, `error_msg` TEXT NULL, `date_add` DATETIME NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_tasks_mail_import_message_key` (`message_key`), INDEX `idx_tasks_mail_import_task` (`task_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8' ); } private function extractMessageContent( $imap, $message_no ) { $structure = @imap_fetchstructure( $imap, $message_no ); $parts = []; $this -> flattenParts( $imap, $message_no, $structure, '', $parts, [] ); $plain = ''; $html = ''; $attachment_candidates = []; foreach ( $parts as $part ) { if ( $part['mime'] === 'text/plain' and $plain === '' ) $plain = $part['body']; if ( $part['mime'] === 'text/html' and $html === '' ) $html = $part['body']; if ( $part['is_attachment'] ) $attachment_candidates[] = $part; } $cid_refs = $this -> extractReferencedCidValues( $html ); $attachments = []; $inline_images = []; foreach ( $attachment_candidates as $part ) { $content_id = isset( $part['content_id'] ) ? (string)$part['content_id'] : ''; // Sprawdź czy to inline image używany w HTML if ( $content_id !== '' && isset( $cid_refs[$content_id] ) ) { $inline_images[$content_id] = [ 'name' => $part['name'], 'content' => $part['body'], 'mime' => isset( $part['mime'] ) ? $part['mime'] : 'application/octet-stream' ]; continue; } $should_import = $this -> shouldImportAttachment( $part, $cid_refs ); if ( !$should_import ) continue; $attachments[] = [ 'name' => $part['name'], 'content' => $part['body'], 'mime' => isset( $part['mime'] ) ? $part['mime'] : 'application/octet-stream' ]; } $text = $plain !== '' ? $plain : $this -> htmlToText( $html ); $text = $this -> cleanBodyText( $text ); return [ 'text' => $text, 'html' => $html, 'inline_images' => $inline_images, 'attachments' => $attachments ]; } private function flattenParts( $imap, $message_no, $structure, $prefix, array &$parts, array $parent_context = [] ) { if ( !$structure ) { $parts[] = [ 'mime' => 'text/plain', 'body' => '', 'name' => '', 'is_attachment' => false, 'is_inline' => false ]; return; } if ( !isset( $structure -> parts ) or !is_array( $structure -> parts ) or !count( $structure -> parts ) ) { $part_number = $prefix !== '' ? $prefix : null; $parts[] = $this -> parseSinglePart( $imap, $message_no, $structure, $part_number, $parent_context ); return; } $current_context = $this -> mergePartContexts( $parent_context, $this -> extractPartContext( $structure ) ); foreach ( $structure -> parts as $index => $subpart ) { $part_number = $prefix === '' ? (string)( $index + 1 ) : $prefix . '.' . ( $index + 1 ); $child_context = $this -> mergePartContexts( $current_context, $this -> extractPartContext( $subpart ) ); if ( isset( $subpart -> parts ) and is_array( $subpart -> parts ) and count( $subpart -> parts ) ) $this -> flattenParts( $imap, $message_no, $subpart, $part_number, $parts, $child_context ); else $parts[] = $this -> parseSinglePart( $imap, $message_no, $subpart, $part_number, $child_context ); } } private function parseSinglePart( $imap, $message_no, $part, $part_number, array $context = [] ) { $raw = ''; if ( $part_number === null ) $raw = (string)@imap_body( $imap, $message_no, FT_PEEK ); else $raw = (string)@imap_fetchbody( $imap, $message_no, $part_number, FT_PEEK ); $mime = $this -> mimeType( $part ); $decoded = $this -> decodePartBody( $raw, isset( $part -> encoding ) ? (int)$part -> encoding : 0 ); if ( strpos( $mime, 'text/' ) === 0 ) $decoded = str_replace( [ "\r\n", "\r" ], "\n", (string)$decoded ); $params = $this -> partParams( $part ); $filename_param = isset( $params['filename'] ) ? $this -> decodeHeaderValue( $params['filename'] ) : ''; $name_param = isset( $params['name'] ) ? $this -> decodeHeaderValue( $params['name'] ) : ''; $description_param = isset( $params['description'] ) ? $this -> decodeHeaderValue( $params['description'] ) : ''; $name = $this -> pickBetterAttachmentName( $filename_param, $name_param ); $name = $this -> pickBetterAttachmentName( $name, $description_param ); if ( isset( $part -> description ) && trim( (string)$part -> description ) !== '' ) $name = $this -> pickBetterAttachmentName( $name, $this -> decodeHeaderValue( (string)$part -> description ) ); if ( isset( $context['name'] ) && trim( (string)$context['name'] ) !== '' ) $name = $this -> pickBetterAttachmentName( $name, (string)$context['name'] ); // Fallback: jeśli brak nazwy, brak rozszerzenia lub nazwa techniczna, spróbuj z surowych nagłówków MIME if ( $part_number !== null && ( trim( $name ) === '' || pathinfo( $name, PATHINFO_EXTENSION ) === '' || self::isTechnicalAttachmentName( $name ) ) ) { $mime_name = $this -> extractNameFromRawMime( $imap, $message_no, $part_number ); if ( $mime_name !== '' ) $name = $mime_name; } $name = self::normalizeImportedAttachmentName( $name, $mime, $decoded ); $disposition = isset( $part -> disposition ) ? strtoupper( trim( (string)$part -> disposition ) ) : ''; if ( $disposition === '' && isset( $context['disposition'] ) ) $disposition = strtoupper( trim( (string)$context['disposition'] ) ); $content_id = ''; if ( isset( $part -> id ) and trim( (string)$part -> id ) !== '' ) $content_id = $this -> normalizeContentId( (string)$part -> id ); elseif ( isset( $params['content-id'] ) and trim( (string)$params['content-id'] ) !== '' ) $content_id = $this -> normalizeContentId( (string)$params['content-id'] ); elseif ( isset( $context['content_id'] ) ) $content_id = $this -> normalizeContentId( (string)$context['content_id'] ); $is_inline = $disposition === 'INLINE' or $content_id !== ''; $is_attachment = trim( $name ) !== '' or $disposition === 'ATTACHMENT' or $this -> isLikelyBinaryAttachmentWithoutName( $mime, $decoded, $disposition, $content_id ); return [ 'mime' => $mime, 'body' => $decoded, 'name' => $name, 'is_attachment' => $is_attachment, 'is_inline' => $is_inline, 'disposition' => $disposition, 'content_id' => $content_id ]; } private function partParams( $part ) { $params = []; if ( isset( $part -> parameters ) and is_array( $part -> parameters ) ) { foreach ( $part -> parameters as $param ) { if ( isset( $param -> attribute ) ) $params[ strtolower( (string)$param -> attribute ) ] = isset( $param -> value ) ? (string)$param -> value : ''; } } if ( isset( $part -> dparameters ) and is_array( $part -> dparameters ) ) { foreach ( $part -> dparameters as $param ) { if ( isset( $param -> attribute ) ) $params[ strtolower( (string)$param -> attribute ) ] = isset( $param -> value ) ? (string)$param -> value : ''; } } return $this -> reassembleRfc2231Params( $params ); } private function reassembleRfc2231Params( array $params ) { $normal = []; $rfc2231 = []; foreach ( $params as $key => $value ) { if ( preg_match( '/^([^*]+)\*(\d+)?\*?$/', $key, $m ) ) { $base = $m[1]; $index = isset( $m[2] ) && $m[2] !== '' ? (int)$m[2] : 0; $rfc2231[$base][$index] = $value; } else $normal[$key] = $value; } foreach ( $rfc2231 as $base => $parts ) { ksort( $parts, SORT_NUMERIC ); $combined = implode( '', $parts ); if ( preg_match( "/^([^']*)'([^']*)'(.+)$/s", $combined, $enc ) ) { $charset = strtoupper( $enc[1] ); $decoded = rawurldecode( $enc[3] ); if ( $charset !== '' && $charset !== 'UTF-8' && function_exists( 'mb_convert_encoding' ) ) $decoded = @mb_convert_encoding( $decoded, 'UTF-8', $charset ); $combined = $decoded; } if ( $combined !== '' || !isset( $normal[$base] ) ) $normal[$base] = $combined; } return $normal; } private function extractNameFromRawMime( $imap, $message_no, $part_number ) { $headers = $this -> fetchRawMimeHeaders( $imap, $message_no, $part_number ); if ( trim( $headers ) === '' ) return ''; return $this -> extractNameFromMimeHeaders( $headers ); } private function extractNameFromMimeHeaders( $headers ) { $headers = (string)$headers; if ( trim( $headers ) === '' ) return ''; $best_name = ''; // Szukaj filename w Content-Disposition, potem name w Content-Type foreach ( [ 'filename', 'name' ] as $param_name ) { // RFC 2231 z kontynuacją: param*0*=...; param*1*=... $rfc2231_parts = []; if ( preg_match_all( '/' . preg_quote( $param_name, '/' ) . '\*(\d+)\*?\s*=\s*([^\r\n;]+)/i', $headers, $matches, PREG_SET_ORDER ) ) { foreach ( $matches as $m ) $rfc2231_parts[(int)$m[1]] = trim( $m[2], " \t\"'" ); if ( !empty( $rfc2231_parts ) ) { ksort( $rfc2231_parts, SORT_NUMERIC ); $combined = implode( '', $rfc2231_parts ); if ( preg_match( "/^([^']*)'([^']*)'(.+)$/s", $combined, $enc ) ) { $charset = strtoupper( $enc[1] ); $decoded = rawurldecode( $enc[3] ); if ( $charset !== '' && $charset !== 'UTF-8' && function_exists( 'mb_convert_encoding' ) ) $decoded = @mb_convert_encoding( $decoded, 'UTF-8', $charset ); $combined = $decoded; } if ( trim( $combined ) !== '' ) $best_name = $this -> pickBetterAttachmentName( $best_name, trim( $combined ) ); } } // RFC 2231 prosty: param*=charset'lang'value if ( preg_match( '/' . preg_quote( $param_name, '/' ) . '\*\s*=\s*([^\r\n;]+)/i', $headers, $m ) ) { $val = trim( $m[1], " \t\"'" ); if ( preg_match( "/^([^']*)'([^']*)'(.+)$/s", $val, $enc ) ) { $charset = strtoupper( $enc[1] ); $decoded = rawurldecode( $enc[3] ); if ( $charset !== '' && $charset !== 'UTF-8' && function_exists( 'mb_convert_encoding' ) ) $decoded = @mb_convert_encoding( $decoded, 'UTF-8', $charset ); $val = $decoded; } if ( trim( $val ) !== '' ) $best_name = $this -> pickBetterAttachmentName( $best_name, trim( $val ) ); } // Standardowy: param="value" lub param=value if ( preg_match( '/' . preg_quote( $param_name, '/' ) . '\s*=\s*"([^"]+)"/i', $headers, $m ) ) { $val = $this -> decodeHeaderValue( trim( $m[1] ) ); if ( trim( $val ) !== '' ) $best_name = $this -> pickBetterAttachmentName( $best_name, trim( $val ) ); } if ( preg_match( '/' . preg_quote( $param_name, '/' ) . '\s*=\s*([^\s;]+)/i', $headers, $m ) ) { $val = $this -> decodeHeaderValue( trim( $m[1], " \t\"'" ) ); if ( trim( $val ) !== '' ) $best_name = $this -> pickBetterAttachmentName( $best_name, trim( $val ) ); } // Wartość bez cudzysłowów, ale ze spacjami aż do końca linii lub średnika if ( preg_match( '/' . preg_quote( $param_name, '/' ) . '\s*=\s*([^;\r\n]+)/i', $headers, $m ) ) { $val = $this -> decodeHeaderValue( trim( $m[1], " \t\"'" ) ); if ( trim( $val ) !== '' ) $best_name = $this -> pickBetterAttachmentName( $best_name, trim( $val ) ); } } if ( preg_match( '/^\s*content-description\s*:\s*(.+)$/im', $headers, $m ) ) { $val = $this -> decodeHeaderValue( trim( $m[1] ) ); if ( trim( $val ) !== '' ) $best_name = $this -> pickBetterAttachmentName( $best_name, trim( $val ) ); } return trim( $best_name ); } public function debugExtractNameFromMimeHeaders( $headers ) { return $this -> extractNameFromMimeHeaders( $headers ); } private function extractPartContext( $part ) { if ( !$part ) return []; $params = $this -> partParams( $part ); $filename_param = isset( $params['filename'] ) ? $this -> decodeHeaderValue( $params['filename'] ) : ''; $name_param = isset( $params['name'] ) ? $this -> decodeHeaderValue( $params['name'] ) : ''; $description_param = isset( $params['description'] ) ? $this -> decodeHeaderValue( $params['description'] ) : ''; $name = $this -> pickBetterAttachmentName( $filename_param, $name_param ); $name = $this -> pickBetterAttachmentName( $name, $description_param ); if ( isset( $part -> description ) && trim( (string)$part -> description ) !== '' ) $name = $this -> pickBetterAttachmentName( $name, $this -> decodeHeaderValue( (string)$part -> description ) ); $disposition = isset( $part -> disposition ) ? strtoupper( trim( (string)$part -> disposition ) ) : ''; $content_id = ''; if ( isset( $part -> id ) and trim( (string)$part -> id ) !== '' ) $content_id = $this -> normalizeContentId( (string)$part -> id ); elseif ( isset( $params['content-id'] ) and trim( (string)$params['content-id'] ) !== '' ) $content_id = $this -> normalizeContentId( (string)$params['content-id'] ); return [ 'name' => $name, 'disposition' => $disposition, 'content_id' => $content_id ]; } private function mergePartContexts( array $parent_context, array $child_context ) { $merged = $parent_context; $parent_name = isset( $parent_context['name'] ) ? (string)$parent_context['name'] : ''; $child_name = isset( $child_context['name'] ) ? (string)$child_context['name'] : ''; $merged['name'] = $this -> pickBetterAttachmentName( $parent_name, $child_name ); if ( isset( $child_context['disposition'] ) && trim( (string)$child_context['disposition'] ) !== '' ) $merged['disposition'] = (string)$child_context['disposition']; elseif ( isset( $parent_context['disposition'] ) ) $merged['disposition'] = (string)$parent_context['disposition']; if ( isset( $child_context['content_id'] ) && trim( (string)$child_context['content_id'] ) !== '' ) $merged['content_id'] = (string)$child_context['content_id']; elseif ( isset( $parent_context['content_id'] ) ) $merged['content_id'] = (string)$parent_context['content_id']; return $merged; } private function fetchRawMimeHeaders( $imap, $message_no, $part_number ) { if ( $part_number === null ) return ''; $headers = (string)@imap_fetchmime( $imap, $message_no, $part_number, FT_PEEK ); if ( trim( $headers ) === '' ) $headers = (string)@imap_fetchbody( $imap, $message_no, $part_number . '.MIME', FT_PEEK ); return (string)$headers; } private function decodePartBody( $raw, $encoding ) { if ( $encoding === 3 ) return (string)base64_decode( $raw ); if ( $encoding === 4 ) return (string)quoted_printable_decode( $raw ); return (string)$raw; } private function mimeType( $part ) { $primary = [ 0 => 'text', 1 => 'multipart', 2 => 'message', 3 => 'application', 4 => 'audio', 5 => 'image', 6 => 'video', 7 => 'other' ]; $type = isset( $part -> type ) ? (int)$part -> type : 0; $subtype = isset( $part -> subtype ) ? strtolower( (string)$part -> subtype ) : 'plain'; return ( isset( $primary[$type] ) ? $primary[$type] : 'text' ) . '/' . $subtype; } private function htmlToText( $html ) { $html = (string)$html; if ( trim( $html ) === '' ) return ''; $html = preg_replace( '/]*>.*?<\/style>/is', ' ', $html ); $html = preg_replace( '/]*>.*?<\/script>/is', ' ', $html ); $html = preg_replace( '/]*>.*?<\/blockquote>/is', ' ', $html ); $html = preg_replace( '/]*class="[^"]*(gmail_quote|gmail_signature)[^"]*"[^>]*>.*?<\/div>/is', ' ', $html ); $html = preg_replace( '//i', "\n", $html ); $html = preg_replace( '/<\/(p|div|li|tr|h[1-6])>/i', "\n", $html ); $text = html_entity_decode( strip_tags( $html ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); return $text; } private function cleanBodyText( $text ) { $text = str_replace( [ "\r\n", "\r" ], "\n", (string)$text ); $text = preg_replace( '/\x{00a0}/u', ' ', $text ); $lines = explode( "\n", $text ); $clean = []; foreach ( $lines as $line ) { $line_trim = rtrim( $line ); if ( preg_match( '/^\s*>+/', $line_trim ) ) break; if ( preg_match( '/^\s*On\s.+wrote:\s*$/i', $line_trim ) ) break; if ( preg_match( '/^\s*W\s+dniu\s+.+napisal\(a\):\s*$/iu', $line_trim ) ) break; if ( preg_match( '/^\s*-----Original Message-----\s*$/i', $line_trim ) ) break; if ( preg_match( '/^\s*Od:\s.+$/iu', $line_trim ) ) break; if ( preg_match( '/^\s*From:\s.+$/i', $line_trim ) ) break; if ( preg_match( '/^\s*Sent:\s.+$/i', $line_trim ) ) break; if ( preg_match( '/^\s*--\s*$/', $line_trim ) ) break; // Polskie stopki if ( preg_match( '/^\s*(Pozdrawiam|Pozdrowienia|Z\s+powa[zż]aniem|Dzi[eę]kuj[eę]|Serdecznie|Miłego\s+dnia)/iu', $line_trim ) ) break; // Angielskie stopki if ( preg_match( '/^\s*(Best\s+regards|Kind\s+regards|Regards|Sincerely|Thanks|Thank\s+you|Cheers)/i', $line_trim ) ) break; // Mobilne sygnatury if ( preg_match( '/^\s*Sent\s+from\s+(my\s+)?(iPhone|iPad|Android|Samsung|mobile)/i', $line_trim ) ) break; // Separatory z podkreślników if ( preg_match( '/^\s*_{3,}\s*$/', $line_trim ) ) break; $clean[] = $line_trim; } $text = trim( implode( "\n", $clean ) ); return trim( $text ); } private function prepareImportedTaskText( $text ) { $text = trim( (string)$text ); if ( $text === '' ) return '(brak tresci)'; if ( preg_match( '/<\s*[a-z][^>]*>/i', $text ) ) { $text = $this -> htmlToText( $text ); $text = $this -> cleanBodyText( $text ); } $text = str_replace( [ "\r\n", "\r" ], "\n", $text ); $text = trim( $text ); if ( $text === '' ) return '(brak tresci)'; return nl2br( $text ); } private function shouldImportAttachment( array $part, array $cid_refs ) { $name = trim( isset( $part['name'] ) ? (string)$part['name'] : '' ); $mime = strtolower( isset( $part['mime'] ) ? (string)$part['mime'] : '' ); $disposition = strtoupper( isset( $part['disposition'] ) ? (string)$part['disposition'] : '' ); $content_id = $this -> normalizeContentId( isset( $part['content_id'] ) ? (string)$part['content_id'] : '' ); $body = isset( $part['body'] ) ? (string)$part['body'] : ''; if ( !empty( $part['is_inline'] ) ) return false; if ( $mime === 'text/plain' or $mime === 'text/html' ) return false; if ( $content_id !== '' and isset( $cid_refs[$content_id] ) ) return false; if ( strpos( $mime, 'image/' ) === 0 and $disposition !== 'ATTACHMENT' ) return false; if ( $name !== '' ) return true; if ( $disposition === 'ATTACHMENT' ) return true; if ( $mime === 'application/pdf' or $mime === 'application/x-pdf' ) return true; if ( $mime === 'application/octet-stream' and self::contentLooksLikePdf( $body ) ) return true; return false; } private function isLikelyBinaryAttachmentWithoutName( $mime, $content, $disposition, $content_id ) { $mime = strtolower( trim( (string)$mime ) ); $disposition = strtoupper( trim( (string)$disposition ) ); $content_id = trim( (string)$content_id ); if ( strpos( $mime, 'text/' ) === 0 ) return false; if ( $content_id !== '' ) return false; if ( strpos( $mime, 'image/' ) === 0 && $disposition !== 'ATTACHMENT' ) return false; if ( $mime === 'application/pdf' || $mime === 'application/x-pdf' ) return true; if ( $mime === 'application/octet-stream' && self::contentLooksLikePdf( $content ) ) return true; return false; } private static function contentLooksLikePdf( $content ) { if ( !is_string( $content ) || $content === '' ) return false; return strpos( substr( $content, 0, 1024 ), '%PDF-' ) !== false; } private function extractReferencedCidValues( $html ) { $html = (string)$html; if ( trim( $html ) === '' ) return []; preg_match_all( '/cid:([^"\'>\s]+)/i', $html, $matches ); if ( !isset( $matches[1] ) or !is_array( $matches[1] ) or !count( $matches[1] ) ) return []; $refs = []; foreach ( $matches[1] as $raw ) { $cid = $this -> normalizeContentId( $raw ); if ( $cid !== '' ) $refs[$cid] = true; } return $refs; } private function normalizeContentId( $value ) { $value = trim( (string)$value ); if ( $value === '' ) return ''; $value = trim( $value, '<>' ); return strtolower( $value ); } private function parseWithAI( $api_key, $model, $subject, $raw_content, $attempt = 1 ) { $this -> last_ai_error = ''; $api_key = trim( (string)$api_key ); $subject = trim( (string)$subject ); $raw_content = trim( (string)$raw_content ); $model = trim( (string)$model ); if ( $api_key === '' ) { $this -> last_ai_error = 'Brak klucza API OpenAI.'; return null; } if ( $model === '' ) $model = 'gpt-4o-mini'; if ( $raw_content === '' ) { $this -> last_ai_error = 'Pusta tresc emaila do analizy AI.'; return null; } if ( !function_exists( 'curl_init' ) ) { $this -> last_ai_error = 'Brak rozszerzenia cURL.'; return null; } // Przygotuj treść do analizy (usuń HTML jesli jest) if ( preg_match( '/<\s*[a-z][^>]*>/i', $raw_content ) ) { $raw_content = $this -> htmlToText( $raw_content ); $raw_content = $this -> cleanBodyText( $raw_content ); } $content_limit = $attempt > 1 ? 1200 : 1800; $prompt = "Jesteś asystentem do przetwarzania emaili na zadania w systemie CRM. " . "Na podstawie poniższego tematu i treści emaila wygeneruj zadanie wdrożeniowe, NIE podsumowanie. " . "Opis ma być szczegółową listą rzeczy do wykonania dla zespołu.\n\n" . "Wymagania:\n" . "- Nie pomijaj żadnych wymagań, liczb, zakresów cen, CTA, pól formularza, kroków i terminów.\n" . "- Zamień treść emaila na checklistę implementacyjną.\n" . "- Zachowaj strukturę sekcji i podsekcji.\n" . "- Pisz konkretnie co zmienić, gdzie zmienić i jakie treści dodać.\n" . "- Nie używaj ogólników typu: \"zaktualizować stronę\".\n\n" . "Format pola task_text:\n" . "1) Cel zmiany\n" . "2) Zakres zmian (lista punktów)\n" . "3) Treści do wstawienia (dokładne teksty/liczby)\n" . "4) Zmiany formularza\n" . "5) Wersja sezonowa / warunki wyświetlania\n" . "6) CTA i bonusy\n" . "7) Kryteria akceptacji\n\n" . "Temat emaila: " . $subject . "\n\n" . "Treść emaila:\n" . mb_substr( $raw_content, 0, $content_limit ) . "\n\n" . "Zwróć TYLKO poprawny JSON:\n" . '{"task_name": "krótki temat zadania", "task_text": "szczegółowa checklista wdrożeniowa"}'; $payload = [ 'model' => $model, 'messages' => [ [ 'role' => 'system', 'content' => 'Jesteś asystentem do przetwarzania emaili. Odpowiadasz TYLKO w formacie JSON i tworzysz szczegółowe checklisty wdrożeniowe, nie skróty.' ], [ 'role' => 'user', 'content' => $prompt ] ] ]; if ( stripos( $model, 'gpt-5' ) === 0 ) $payload['response_format'] = [ 'type' => 'json_object' ]; if ( stripos( $model, 'gpt-5' ) === 0 ) $payload['max_completion_tokens'] = $attempt > 1 ? 1600 : 1000; else { $payload['temperature'] = 0.3; $payload['max_tokens'] = 1200; } $ch = curl_init( 'https://api.openai.com/v1/chat/completions' ); curl_setopt_array( $ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode( $payload ), CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . $api_key ], CURLOPT_TIMEOUT => 30 ] ); $response = curl_exec( $ch ); $curl_error = curl_error( $ch ); $http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE ); curl_close( $ch ); if ( $response === false ) { $this -> last_ai_error = 'Blad cURL: ' . $curl_error; return null; } if ( $http_code !== 200 ) { $this -> last_ai_error = 'HTTP ' . $http_code . ' z OpenAI: ' . $this -> extractApiErrorMessage( $response ); return null; } if ( !$response ) { $this -> last_ai_error = 'Pusta odpowiedz z OpenAI.'; return null; } $data = json_decode( $response, true ); if ( !is_array( $data ) ) { $this -> last_ai_error = 'Niepoprawny JSON z OpenAI: ' . json_last_error_msg(); return null; } if ( isset( $data['error']['message'] ) ) { $this -> last_ai_error = 'OpenAI error: ' . (string)$data['error']['message']; return null; } if ( !isset( $data['choices'][0] ) || !is_array( $data['choices'][0] ) ) { $this -> last_ai_error = 'Brak choices[0] w odpowiedzi OpenAI. RAW: ' . $this -> clipForLog( $response ); return null; } $choice = $data['choices'][0]; $content = $this -> extractModelMessageContent( $choice ); if ( $content === '' ) { $finish_reason = isset( $choice['finish_reason'] ) ? (string)$choice['finish_reason'] : '(brak)'; $refusal = isset( $choice['message']['refusal'] ) ? (string)$choice['message']['refusal'] : ''; if ( $finish_reason === 'length' && (int)$attempt === 1 ) { $retry_content = mb_substr( $raw_content, 0, 1200 ); return $this -> parseWithAI( $api_key, $model, $subject, $retry_content, 2 ); } $error = 'Pusta tresc odpowiedzi modelu. finish_reason=' . $finish_reason; if ( $refusal !== '' ) $error .= ', refusal=' . $this -> clipForLog( $refusal ); $this -> last_ai_error = $error . '. RAW: ' . $this -> clipForLog( $response ); return null; } // Usuń markdown code block jeśli jest $content = preg_replace( '/```json\s*|\s*```/', '', $content ); $content = trim( $content ); // Jeśli model zwrócił JSON z komentarzem wokół, wyciągnij tylko obiekt. $json_start = strpos( $content, '{' ); $json_end = strrpos( $content, '}' ); if ( $json_start !== false && $json_end !== false && $json_end > $json_start ) $content = substr( $content, $json_start, $json_end - $json_start + 1 ); $parsed = json_decode( $content, true ); if ( !is_array( $parsed ) || !isset( $parsed['task_name'] ) || !isset( $parsed['task_text'] ) ) { $this -> last_ai_error = 'Niepoprawny JSON z modelu. Odpowiedz: ' . $this -> clipForLog( $content ); return null; } $task_name = $this -> normalizeAiTextValue( $parsed['task_name'] ); $task_text = $this -> normalizeAiTextValue( $parsed['task_text'] ); if ( $task_name === '' ) $task_name = '(bez tematu)'; if ( $task_text === '' ) $task_text = '(brak treści)'; // Formatuj treść zadania z nl2br $task_text = nl2br( $task_text ); return [ 'task_name' => $task_name, 'task_text' => $task_text ]; } private function extractApiErrorMessage( $response ) { $data = json_decode( (string)$response, true ); if ( is_array( $data ) && isset( $data['error']['message'] ) ) return (string)$data['error']['message']; return $this -> clipForLog( $response ); } private function extractModelMessageContent( array $choice ) { if ( isset( $choice['message']['content'] ) && is_string( $choice['message']['content'] ) ) return trim( $choice['message']['content'] ); if ( isset( $choice['message']['content'] ) && is_array( $choice['message']['content'] ) ) { $chunks = []; foreach ( $choice['message']['content'] as $part ) { if ( is_string( $part ) ) $chunks[] = $part; elseif ( is_array( $part ) ) { if ( isset( $part['text'] ) && is_string( $part['text'] ) ) $chunks[] = $part['text']; elseif ( isset( $part['text']['value'] ) && is_string( $part['text']['value'] ) ) $chunks[] = $part['text']['value']; } } return trim( implode( "\n", $chunks ) ); } if ( isset( $choice['message']['tool_calls'][0]['function']['arguments'] ) && is_string( $choice['message']['tool_calls'][0]['function']['arguments'] ) ) return trim( $choice['message']['tool_calls'][0]['function']['arguments'] ); if ( isset( $choice['message']['function_call']['arguments'] ) && is_string( $choice['message']['function_call']['arguments'] ) ) return trim( $choice['message']['function_call']['arguments'] ); return ''; } private function clipForLog( $value, $limit = 600 ) { $text = trim( (string)$value ); if ( $text === '' ) return ''; return mb_substr( $text, 0, (int)$limit ); } private function normalizeAiTextValue( $value ) { if ( is_string( $value ) ) return trim( $value ); if ( is_numeric( $value ) || is_bool( $value ) ) return trim( (string)$value ); if ( is_array( $value ) ) { $lines = []; foreach ( $value as $item ) { $item_text = $this -> normalizeAiTextValue( $item ); if ( $item_text !== '' ) $lines[] = $item_text; } return trim( implode( "\n", $lines ) ); } if ( is_object( $value ) ) { $array_value = json_decode( json_encode( $value ), true ); if ( is_array( $array_value ) ) { $normalized = $this -> normalizeAiTextValue( $array_value ); if ( $normalized !== '' ) return $normalized; } return trim( json_encode( $value ) ); } return ''; } private function guessExtensionFromMime( $mime ) { $ext = $this -> extensionFromMime( $mime ); return $ext !== '' ? $ext : 'png'; } private function extensionFromMime( $mime ) { $map = [ 'application/pdf' => 'pdf', 'application/zip' => 'zip', 'application/x-rar-compressed' => 'rar', 'application/vnd.rar' => 'rar', 'application/x-7z-compressed' => '7z', 'application/msword' => 'doc', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', 'application/vnd.ms-excel' => 'xls', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', 'application/vnd.ms-powerpoint' => 'ppt', 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', 'application/xml' => 'xml', 'application/json' => 'json', 'application/rtf' => 'rtf', 'application/octet-stream' => '', 'text/plain' => 'txt', 'text/html' => 'html', 'text/csv' => 'csv', 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', 'image/bmp' => 'bmp', 'image/svg+xml' => 'svg', 'image/tiff' => 'tiff' ]; $mime = strtolower( trim( (string)$mime ) ); return isset( $map[$mime] ) ? $map[$mime] : ''; } private function pickBetterAttachmentName( $primary, $secondary ) { $primary = trim( (string)$primary ); $secondary = trim( (string)$secondary ); if ( $primary === '' ) return $secondary; if ( $secondary === '' ) return $primary; $primary_score = self::attachmentNameScore( $primary ); $secondary_score = self::attachmentNameScore( $secondary ); return $secondary_score > $primary_score ? $secondary : $primary; } public static function normalizeImportedAttachmentName( $name, $mime = '', $content = null ) { $name = trim( (string)$name ); $technical_ext = ''; if ( self::isTechnicalAttachmentName( $name ) ) { $technical_ext = strtolower( pathinfo( $name, PATHINFO_EXTENSION ) ); $name = ''; } if ( $name === '' ) $name = 'zalacznik'; $ext = strtolower( pathinfo( $name, PATHINFO_EXTENSION ) ); if ( $ext === '' ) { $mime_ext = self::extensionFromMimeStatic( $mime ); if ( $mime_ext === '' && $technical_ext !== '' ) $mime_ext = $technical_ext; if ( $mime_ext === '' ) $mime_ext = self::extensionFromContent( $content ); if ( $mime_ext !== '' ) $name .= '.' . $mime_ext; } return $name; } private static function attachmentNameScore( $name ) { $name = trim( (string)$name ); if ( $name === '' ) return -1; $score = 1; if ( pathinfo( $name, PATHINFO_EXTENSION ) !== '' ) $score += 2; if ( self::isTechnicalAttachmentName( $name ) ) $score -= 3; return $score; } private static function isTechnicalAttachmentName( $name ) { $name = trim( (string)$name ); if ( $name === '' ) return false; return (bool)preg_match( '/^att_[a-f0-9]{8,}(?:\.[a-z0-9]{2,6})?$/i', $name ); } private static function extensionFromMimeStatic( $mime ) { $map = [ 'application/pdf' => 'pdf', 'application/zip' => 'zip', 'application/x-rar-compressed' => 'rar', 'application/vnd.rar' => 'rar', 'application/x-7z-compressed' => '7z', 'application/msword' => 'doc', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', 'application/vnd.ms-excel' => 'xls', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', 'application/vnd.ms-powerpoint' => 'ppt', 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', 'application/xml' => 'xml', 'application/json' => 'json', 'application/rtf' => 'rtf', 'application/octet-stream' => '', 'text/plain' => 'txt', 'text/html' => 'html', 'text/csv' => 'csv', 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', 'image/bmp' => 'bmp', 'image/svg+xml' => 'svg', 'image/tiff' => 'tiff' ]; $mime = strtolower( trim( (string)$mime ) ); return isset( $map[$mime] ) ? $map[$mime] : ''; } private static function extensionFromContent( $content ) { if ( !is_string( $content ) || $content === '' ) return ''; $prefix = substr( $content, 0, 16 ); if ( substr( $prefix, 0, 4 ) === '%PDF' ) return 'pdf'; if ( substr( $prefix, 0, 2 ) === 'PK' ) return 'zip'; if ( substr( $prefix, 0, 3 ) === "\xFF\xD8\xFF" ) return 'jpg'; if ( substr( $prefix, 0, 8 ) === "\x89PNG\r\n\x1A\n" ) return 'png'; if ( substr( $prefix, 0, 6 ) === 'GIF87a' || substr( $prefix, 0, 6 ) === 'GIF89a' ) return 'gif'; return ''; } private function replaceCidReferences( $html, array $cid_to_url ) { if ( trim( (string)$html ) === '' || empty( $cid_to_url ) ) return $html; foreach ( $cid_to_url as $cid => $url ) { // Zastąp wszystkie odniesienia do tego CID $html = preg_replace( '/(]+src=["\'])cid:' . preg_quote( $cid, '/' ) . '(["\'][^>]*>)/i', '$1' . $url . '$2', $html ); } return $html; } private function prepareImportedTaskTextFromHtml( $html ) { $html = trim( (string)$html ); if ( $html === '' ) return '(brak tresci)'; // Usuń niepotrzebne elementy (style, script, blockquote) $html = preg_replace( '/]*>.*?<\/style>/is', ' ', $html ); $html = preg_replace( '/]*>.*?<\/script>/is', ' ', $html ); $html = preg_replace( '/]*>.*?<\/blockquote>/is', ' ', $html ); $html = preg_replace( '/]*class="[^"]*(gmail_quote|gmail_signature)[^"]*"[^>]*>.*?<\/div>/is', ' ', $html ); // Usuń tagi MS Office: , , , itp. $html = preg_replace( '/<\/?[a-z]+:[^>]*>/is', '', $html ); // Usuń xmlns atrybuty $html = preg_replace( '/\s+xmlns:[a-z]+="[^"]*"/i', '', $html ); // Usuń atrybuty class (MsoNormal, itp.) $html = preg_replace( '/\s+class="[^"]*"/i', '', $html ); // Usuń atrybuty style $html = preg_replace( '/\s+style="[^"]*"/i', '', $html ); // Usuń puste paragrafy (z samym   lub spacjami) $html = preg_replace( '/]*>\s*( |\xC2\xA0)?\s*<\/p>/i', '', $html ); // Usuń niepotrzebne atrybuty z obrazów (zostaw tylko src, alt, width, height) $html = preg_replace_callback( '/]*)>/i', function( $matches ) { $attrs = $matches[1]; $new_attrs = []; if ( preg_match( '/\bsrc=["\']([^"\']+)["\']/i', $attrs, $src ) ) $new_attrs[] = 'src="' . htmlspecialchars( $src[1], ENT_QUOTES ) . '"'; if ( preg_match( '/\balt=["\']([^"\']+)["\']/i', $attrs, $alt ) ) $new_attrs[] = 'alt="' . htmlspecialchars( $alt[1], ENT_QUOTES ) . '"'; if ( preg_match( '/\bwidth=["\']?(\d+)["\']?/i', $attrs, $width ) ) $new_attrs[] = 'width="' . (int)$width[1] . '"'; if ( preg_match( '/\bheight=["\']?(\d+)["\']?/i', $attrs, $height ) ) $new_attrs[] = 'height="' . (int)$height[1] . '"'; return ''; }, $html ); // Wyczyść HTML z nadmiarowych białych znaków $html = preg_replace( '/\s+/', ' ', $html ); $html = trim( $html ); if ( $html === '' ) return '(brak tresci)'; return $html; } }