From 203a45e199e3bea8b6f06c17afd40556dc942fcd Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Wed, 18 Feb 2026 21:50:42 +0100 Subject: [PATCH] feat: Update MailToTaskImporter to handle attachment MIME types and improve filename handling; add CLAUDE.md for project guidance; create TODO.md for PDF attachment issue --- .claude/settings.local.json | 3 +- .vscode/ftp-kr.sync.cache.json | 18 +-- CLAUDE.md | 100 ++++++++++++ TODO.md | 1 + autoload/Domain/Tasks/MailToTaskImporter.php | 158 ++++++++++++++++++- templates/tasks/task_popup.php | 2 +- 6 files changed, 267 insertions(+), 15 deletions(-) create mode 100644 CLAUDE.md create mode 100644 TODO.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 9bb0b23..cedea7f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -11,7 +11,8 @@ "Bash(grep:*)", "WebFetch(domain:storefront.developers.shoper.pl)", "WebFetch(domain:developers.shoper.pl)", - "WebFetch(domain:pomoc.home.pl)" + "WebFetch(domain:pomoc.home.pl)", + "Bash(cd:*)" ] } } diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index cf13971..9c0673f 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -121,8 +121,8 @@ }, "class.Tasks.php": { "type": "-", - "size": 23567, - "lmtime": 1771241116941, + "size": 24179, + "lmtime": 1771336367894, "modified": false }, "class.Users.php": { @@ -251,8 +251,8 @@ ".claude": { "settings.local.json": { "type": "-", - "size": 301, - "lmtime": 1771236164946, + "size": 449, + "lmtime": 1771336223831, "modified": false } }, @@ -323,9 +323,9 @@ }, "main_view.php": { "type": "-", - "size": 42781, - "lmtime": 1770583657535, - "modified": true + "size": 42784, + "lmtime": 1771336223833, + "modified": false }, "task_edit.php": { "type": "-", @@ -335,8 +335,8 @@ }, "task_popup.php": { "type": "-", - "size": 20929, - "lmtime": 1771241165284, + "size": 22706, + "lmtime": 1771336355877, "modified": false }, "task_single.php": { diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fbf4fe2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,100 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +CRM PRO is a Polish-language CRM application for task/project management, client management, finances, and work time tracking. Built with PHP + MySQL, Bootstrap, jQuery, and PHP templating. + +## Running the Application + +- **Web entry point:** `index.php` — routes via `?module=&action=` query params +- **AJAX endpoint:** `ajax.php` +- **REST API:** `api.php` +- **Background jobs:** `cron.php` (email import, recursive tasks, reminders) +- **Tests:** `php tests/run.php` (custom lightweight test runner, no PHPUnit) +- **SCSS:** compiled via VS Code Live Sass Compile extension + +## Architecture + +### Layered structure with ongoing DDD migration + +``` +autoload/ +├── Controllers/ # NEW: PSR-4 namespaced, camelCase methods +├── controls/ # LEGACY: snake_case methods, being gradually replaced +├── factory/ # Data access + business logic (legacy, being replaced by Domain) +├── Domain/ # NEW: Repository pattern, single-responsibility classes +│ ├── Tasks/ # WorkTimeRepository, TaskAttachmentRepository, MailToTaskImporter +│ ├── Crm/ # ClientRepository +│ ├── Finances/ # FinanceRepository +│ └── Users/ # UserRepository +├── view/ # View rendering layer +├── class.S.php # Global static utility (sessions, request params, email, hashing) +├── class.Tpl.php # Template engine: Tpl::view('path', $data) +├── class.DbModel.php # Simple ActiveRecord wrapper +└── class.Html.php # HTML form helper components +templates/ # PHP templates organized by module +templates_user/ # Custom user template overrides +``` + +### Routing (`controls\Site::route()`) + +1. Takes `module` and `action` GET params +2. Tries `\Controllers\{Module}Controller::{camelCaseAction}()` first +3. Falls back to `\controls\{Module}::{snake_case_action}()` + +### Autoloading + +Custom `spl_autoload_register` in `index.php`: maps `Namespace\Class` → `autoload/Namespace/Class.php`, falling back to `autoload/Namespace/class.Class.php`. + +### Database access + +- **Medoo** (`$mdb` global) — primary query builder for SELECT/INSERT/UPDATE/DELETE +- **RedBean** (`\R`) — ORM used for some entity operations +- Both configured in `index.php` from `config.php` credentials + +### Key globals + +- `$mdb` — Medoo database instance +- `$user` — current session user array (`\S::get_session('user')`) +- `$settings` — merged app settings from `config.php` + DB `settings` table +- `\S::get('param')` — safe request parameter access + +## Refactoring Status (see REFACTORING_PLAN.md) + +- **Stage 1 (DONE):** Tasks/WorkTime migrated to `Domain\Tasks\WorkTimeRepository` +- **Stage 2 (IN PROGRESS):** Controller standardization — `TasksController` partially migrated +- **Stage 3 (DONE):** UI cleanup for work time billing +- **Stage 4 (NEXT):** Finance domain extraction +- **Stage 5 (NEXT):** View layer standardization + +### Migration rules + +- No big-bang rewrites — one functional area per commit +- New code goes in `Domain/` (repositories) and `Controllers/` (camelCase) +- Legacy `controls/` and `factory/` kept as adapters until full migration +- Every migrated method must have at least one test in `tests/` + +## Coding Conventions + +- **New controllers:** `Controllers\{Module}Controller` with camelCase methods +- **New domain code:** `Domain\{Module}\{Name}Repository` with constructor-injected `$mdb` +- **Legacy code:** `controls\{Module}` and `factory\{Module}` with snake_case methods +- **Templates:** rendered via `\Tpl::view('module/template', $data_array)`, XSS protection via `\Tpl::secureHTML()` +- **UI language:** Polish (labels, comments, database content) +- **File naming:** new classes `ClassName.php`, legacy classes `class.ClassName.php` + +## Key Database Tables + +- `tasks`, `tasks_work`, `tasks_attachments`, `task_user`, `task_action` — task management +- `crm_client` — client records +- `finance_operations`, `finance_categories` — finances +- `users`, `users_permissions` — auth and RBAC +- `tasks_filtrs` — saved user filters + +## Authentication + +- Email + password with PHP sessions, IP validation, cookie-based remember-me +- Permission checks via `\controls\Users::permissions($user_id, $module)` +- Admin (user ID 1) can impersonate other users diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..4414e15 --- /dev/null +++ b/TODO.md @@ -0,0 +1 @@ +1. W wiadomości tworzonej z maila pliki PDF dodały się jako att_6995b8ad9d4567. Trzeba to poprawić. \ No newline at end of file diff --git a/autoload/Domain/Tasks/MailToTaskImporter.php b/autoload/Domain/Tasks/MailToTaskImporter.php index 4400e75..7973888 100644 --- a/autoload/Domain/Tasks/MailToTaskImporter.php +++ b/autoload/Domain/Tasks/MailToTaskImporter.php @@ -204,7 +204,18 @@ class MailToTaskImporter // Zapisz normalne załączniki foreach ( $content['attachments'] as $attachment ) { - $this -> attachments -> uploadFromContent( $task_id, self::TASK_USER_ID, $attachment['name'], $attachment['content'] ); + $att_name = $attachment['name']; + $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'; @@ -466,7 +477,8 @@ class MailToTaskImporter $attachments[] = [ 'name' => $part['name'], - 'content' => $part['body'] + 'content' => $part['body'], + 'mime' => isset( $part['mime'] ) ? $part['mime'] : 'application/octet-stream' ]; } @@ -535,6 +547,14 @@ class MailToTaskImporter $name = $this -> decodeHeaderValue( $name ); + // Fallback: jeśli brak nazwy lub brak rozszerzenia, spróbuj z surowych nagłówków MIME + if ( $part_number !== null && ( trim( $name ) === '' || pathinfo( $name, PATHINFO_EXTENSION ) === '' ) ) + { + $mime_name = $this -> extractNameFromRawMime( $imap, $message_no, $part_number ); + if ( $mime_name !== '' ) + $name = $mime_name; + } + $disposition = isset( $part -> disposition ) ? strtoupper( trim( (string)$part -> disposition ) ) : ''; $content_id = ''; if ( isset( $part -> id ) and trim( (string)$part -> id ) !== '' ) @@ -578,7 +598,113 @@ class MailToTaskImporter } } - return $params; + 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 = (string)@imap_fetchmime( $imap, $message_no, $part_number, FT_PEEK ); + if ( trim( $headers ) === '' ) + return ''; + + // 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 ) !== '' && pathinfo( $combined, PATHINFO_EXTENSION ) !== '' ) + return 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 ) !== '' && pathinfo( $val, PATHINFO_EXTENSION ) !== '' ) + return 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 ) !== '' && pathinfo( $val, PATHINFO_EXTENSION ) !== '' ) + return 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 ) !== '' && pathinfo( $val, PATHINFO_EXTENSION ) !== '' ) + return trim( $val ); + } + } + + return ''; } private function decodePartBody( $raw, $encoding ) @@ -1048,8 +1174,32 @@ class MailToTaskImporter } 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', @@ -1060,7 +1210,7 @@ class MailToTaskImporter ]; $mime = strtolower( trim( (string)$mime ) ); - return isset( $map[$mime] ) ? $map[$mime] : 'png'; + return isset( $map[$mime] ) ? $map[$mime] : ''; } private function replaceCidReferences( $html, array $cid_to_url ) diff --git a/templates/tasks/task_popup.php b/templates/tasks/task_popup.php index c80009d..711517d 100644 --- a/templates/tasks/task_popup.php +++ b/templates/tasks/task_popup.php @@ -98,7 +98,7 @@ task_attachments ) and count( $this -> task_attachments ) ):?> task_attachments as $attachment ):?>
  • - + ()