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

This commit is contained in:
2026-02-18 21:50:42 +01:00
parent c165a94016
commit 203a45e199
6 changed files with 267 additions and 15 deletions

View File

@@ -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:*)"
]
}
}

View File

@@ -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": {

100
CLAUDE.md Normal file
View File

@@ -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=<module>&action=<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

1
TODO.md Normal file
View File

@@ -0,0 +1 @@
1. W wiadomości tworzonej z maila pliki PDF dodały się jako att_6995b8ad9d4567. Trzeba to poprawić.

View File

@@ -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 )

View File

@@ -98,7 +98,7 @@
<? if ( is_array( $this -> task_attachments ) and count( $this -> task_attachments ) ):?>
<? foreach ( $this -> task_attachments as $attachment ):?>
<li>
<a href="<?= $attachment['url'];?>" target="_blank" rel="noopener noreferrer" class="attachment-link">
<a href="<?= $attachment['url'];?>" target="_blank" rel="noopener noreferrer" class="attachment-link" download="<?= htmlspecialchars( $attachment['title_effective'] );?>">
<?= htmlspecialchars( $attachment['title_effective'] );?>
</a>
<small>(<?= $attachment['size_human'];?>)</small>