From 67ed085b381ff2d3128b1e7e9c50ff93629c6cd7 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Tue, 10 Feb 2026 15:37:27 +0100 Subject: [PATCH] feat: Enhance MailToTaskImporter with inline image support and update task content handling --- .claude/settings.local.json | 8 +- .vscode/ftp-kr.sync.cache.json | 12 +- autoload/Domain/Tasks/MailToTaskImporter.php | 146 +++++++++++++++++- .../Domain/Tasks/TaskAttachmentRepository.php | 3 +- config.php | 2 +- templates/html/textarea.php | 5 +- templates/projects/task-edit.php | 3 +- templates/tasks/task_edit.php | 3 +- 8 files changed, 169 insertions(+), 13 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 9a74a56..0751336 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,13 @@ { "permissions": { "allow": [ - "Bash(git log:*)" + "Bash(git log:*)", + "Bash(git checkout:*)", + "Bash(mysql:*)", + "Bash(if [ -f logs.txt ])", + "Bash(then rm logs.txt)", + "Bash(else echo \"Plik logs.txt nie istnieje\")", + "Bash(fi)" ] } } diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index fc542cc..52ab8b9 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -151,9 +151,9 @@ }, "config.php": { "type": "-", - "size": 1249, + "size": 1230, "lmtime": 1770587027872, - "modified": false + "modified": true }, "cron.php": { "type": "-", @@ -204,9 +204,9 @@ }, "main_view.php": { "type": "-", - "size": 42341, + "size": 42781, "lmtime": 1770583657535, - "modified": false + "modified": true }, "task_edit.php": { "type": "-", @@ -216,8 +216,8 @@ }, "task_popup.php": { "type": "-", - "size": 11071, - "lmtime": 0, + "size": 12356, + "lmtime": 1770711646996, "modified": false }, "task_single.php": { diff --git a/autoload/Domain/Tasks/MailToTaskImporter.php b/autoload/Domain/Tasks/MailToTaskImporter.php index 7e92549..da6be39 100644 --- a/autoload/Domain/Tasks/MailToTaskImporter.php +++ b/autoload/Domain/Tasks/MailToTaskImporter.php @@ -154,7 +154,7 @@ class MailToTaskImporter null, false, 'off', - false, + true, self::TASK_STATUS_ID, 'on', 0 @@ -172,6 +172,30 @@ class MailToTaskImporter continue; } + // Konwertuj inline images do Base64 i osadź w treści + $cid_to_data_uri = []; + if ( isset( $content['inline_images'] ) && is_array( $content['inline_images'] ) ) + { + foreach ( $content['inline_images'] as $cid => $inline_image ) + { + // Konwertuj obraz do Base64 data URI + $data_uri = $this -> convertToDataUri( $inline_image['content'], $inline_image['mime'] ); + if ( $data_uri !== '' ) + $cid_to_data_uri[$cid] = $data_uri; + } + } + + // Jeśli są inline images i HTML, zastąp CIDy na Base64 data URI i użyj HTML jako treść + if ( !empty( $cid_to_data_uri ) && isset( $content['html'] ) && trim( $content['html'] ) !== '' ) + { + $html_with_images = $this -> replaceCidReferences( $content['html'], $cid_to_data_uri ); + $task_text = $this -> prepareImportedTaskTextFromHtml( $html_with_images ); + + // Zaktualizuj treść zadania + $this -> mdb -> update( 'tasks', [ 'text' => $task_text ], [ 'id' => $task_id ] ); + } + + // Zapisz normalne załączniki foreach ( $content['attachments'] as $attachment ) { $this -> attachments -> uploadFromContent( $task_id, self::TASK_USER_ID, $attachment['name'], $attachment['content'] ); @@ -412,9 +436,25 @@ class MailToTaskImporter } $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; + } + if ( !$this -> shouldImportAttachment( $part, $cid_refs ) ) continue; @@ -429,6 +469,8 @@ class MailToTaskImporter return [ 'text' => $text, + 'html' => $html, + 'inline_images' => $inline_images, 'attachments' => $attachments ]; } @@ -616,6 +658,22 @@ class MailToTaskImporter 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; } @@ -982,4 +1040,90 @@ class MailToTaskImporter return ''; } + + private function convertToDataUri( $content, $mime_type ) + { + $content = (string)$content; + if ( $content === '' ) + return ''; + + $mime_type = trim( (string)$mime_type ); + if ( $mime_type === '' || $mime_type === 'application/octet-stream' ) + $mime_type = 'image/png'; // Domyślny typ dla obrazów + + $base64 = base64_encode( $content ); + return 'data:' . $mime_type . ';base64,' . $base64; + } + + 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ń niepotrzebne atrybuty z obrazów (zostaw tylko src, alt, width, height) + $html = preg_replace_callback( + '/]*)>/i', + function( $matches ) { + $attrs = $matches[1]; + $new_attrs = []; + + // Wyciągnij ważne atrybuty + if ( preg_match( '/\bsrc=["\']([^"\']+)["\']/i', $attrs, $src ) ) + { + // Nie używaj htmlspecialchars dla data URI (Base64), tylko dla zwykłych URLi + $src_value = $src[1]; + if ( strpos( $src_value, 'data:' ) !== 0 ) + $src_value = htmlspecialchars( $src_value, ENT_QUOTES ); + + $new_attrs[] = 'src="' . $src_value . '"'; + } + + 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; + } } diff --git a/autoload/Domain/Tasks/TaskAttachmentRepository.php b/autoload/Domain/Tasks/TaskAttachmentRepository.php index 0a0acf6..0bdf184 100644 --- a/autoload/Domain/Tasks/TaskAttachmentRepository.php +++ b/autoload/Domain/Tasks/TaskAttachmentRepository.php @@ -224,7 +224,8 @@ class TaskAttachmentRepository if ( !$insert ) return [ 'status' => 'error', 'msg' => 'Nie udalo sie zapisac zalacznika w bazie.' ]; - return [ 'status' => 'success' ]; + $attachment_id = $this -> mdb -> id(); + return [ 'status' => 'success', 'id' => (int)$attachment_id, 'relative_path' => $relative_path, 'stored_name' => $stored_name ]; } private function buildPublicUrl( $relative_path, $stored_name ) diff --git a/config.php b/config.php index 74f8faa..f21b2c0 100644 --- a/config.php +++ b/config.php @@ -21,5 +21,5 @@ $settings['tasks_auto_start_timer'] = false; // OpenAI ChatGPT API configuration for email task parsing $settings['openai_api_key'] = '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'; // Wklej tutaj swój klucz API OpenAI -$settings['openai_parse_emails'] = true; // true = użyj AI do parsowania emaili, false = normalne parsowanie +$settings['openai_parse_emails'] = false; // true = użyj AI do parsowania emaili, false = normalne parsowanie $settings['openai_model'] = 'gpt-4o-mini'; // Model: gpt-4o-mini, gpt-4o, gpt-5-nano, itp. diff --git a/templates/html/textarea.php b/templates/html/textarea.php index 3f3373a..17ba54c 100644 --- a/templates/html/textarea.php +++ b/templates/html/textarea.php @@ -27,7 +27,10 @@ if ( $this -> params['label'] ) if ( $this -> params['placeholder'] ) $out .= 'placeholder="' . $this -> params['placeholder'] . '" '; - $out .= ' rows="' . $this -> params['rows'] . '">' . $this -> secureHTML( $this -> params['value'] ) . ''; + if ( isset( $this -> params['skip_encoding'] ) && $this -> params['skip_encoding'] ) + $out .= ' rows="' . $this -> params['rows'] . '">' . $this -> params['value'] . ''; + else + $out .= ' rows="' . $this -> params['rows'] . '">' . $this -> secureHTML( $this -> params['value'] ) . ''; if ( $this -> params['label'] ) { diff --git a/templates/projects/task-edit.php b/templates/projects/task-edit.php index fe877b6..fe55630 100644 --- a/templates/projects/task-edit.php +++ b/templates/projects/task-edit.php @@ -22,7 +22,8 @@ ob_start(); 'name' => 'text', 'id' => 'text', 'value' => $this -> task[ 'text' ], - 'rows' => 10 + 'rows' => 10, + 'skip_encoding' => true ] ); ?> diff --git a/templates/tasks/task_edit.php b/templates/tasks/task_edit.php index d015bf7..a69b127 100644 --- a/templates/tasks/task_edit.php +++ b/templates/tasks/task_edit.php @@ -22,7 +22,8 @@ ob_start(); 'name' => 'text', 'id' => 'text', 'value' => $this -> task[ 'text' ], - 'rows' => 10 + 'rows' => 10, + 'skip_encoding' => true ] ); ?>