mdb = $mdb; else { global $mdb; $this -> mdb = $mdb; } $this -> attachments = new TaskAttachmentRepository( $this -> mdb ); } 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 ); // 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 !== '' ) { $model = isset( $settings['openai_model'] ) ? trim( (string)$settings['openai_model'] ) : 'gpt-3.5-turbo'; $ai_result = $this -> parseWithAI( $api_key, $model, $subject, $content['text'] ); if ( $ai_result !== null ) { $task_name = $ai_result['task_name']; $task_text = $ai_result['task_text']; } else { // Fallback do normalnego parsowania jeśli AI nie zadziała $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, 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', false, 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; } foreach ( $content['attachments'] as $attachment ) { $this -> attachments -> uploadFromContent( $task_id, self::TASK_USER_ID, $attachment['name'], $attachment['content'] ); } $this -> saveImportLog( $message_key, $task_id, $sender, $subject, 'imported', null ); $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', '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 ''; $chunks = @imap_mime_header_decode( $value ); if ( !is_array( $chunks ) or !count( $chunks ) ) return trim( $value ); $decoded = ''; foreach ( $chunks as $chunk ) $decoded .= isset( $chunk -> text ) ? $chunk -> text : ''; return trim( $decoded ); } 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', '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 = []; foreach ( $attachment_candidates as $part ) { if ( !$this -> shouldImportAttachment( $part, $cid_refs ) ) continue; $attachments[] = [ 'name' => $part['name'], 'content' => $part['body'] ]; } $text = $plain !== '' ? $plain : $this -> htmlToText( $html ); $text = $this -> cleanBodyText( $text ); return [ 'text' => $text, 'attachments' => $attachments ]; } private function flattenParts( $imap, $message_no, $structure, $prefix, array &$parts ) { 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 ); return; } foreach ( $structure -> parts as $index => $subpart ) { $part_number = $prefix === '' ? (string)( $index + 1 ) : $prefix . '.' . ( $index + 1 ); if ( isset( $subpart -> parts ) and is_array( $subpart -> parts ) and count( $subpart -> parts ) ) $this -> flattenParts( $imap, $message_no, $subpart, $part_number, $parts ); else $parts[] = $this -> parseSinglePart( $imap, $message_no, $subpart, $part_number ); } } private function parseSinglePart( $imap, $message_no, $part, $part_number ) { $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 ); $name = ''; if ( isset( $params['filename'] ) ) $name = $params['filename']; elseif ( isset( $params['name'] ) ) $name = $params['name']; $name = $this -> decodeHeaderValue( $name ); $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'] ); $is_inline = $disposition === 'INLINE' or $content_id !== ''; $is_attachment = trim( $name ) !== '' or $disposition === 'ATTACHMENT'; 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 $params; } 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; $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'] : '' ); if ( $name === '' ) return false; $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'] : '' ); 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; return true; } 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 ) { $subject = trim( (string)$subject ); $raw_content = trim( (string)$raw_content ); $model = trim( (string)$model ); if ( $model === '' ) $model = 'gpt-3.5-turbo'; // 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 ); } $prompt = "Jesteś asystentem do przetwarzania emaili na zadania w systemie CRM. " . "Na podstawie poniższego tematu i treści emaila, wygeneruj:\n" . "1. Zwięzły, konkretny temat zadania (max 100 znaków)\n" . "2. Czytelny opis zadania zawierający najważniejsze informacje\n\n" . "Temat emaila: " . $subject . "\n\n" . "Treść emaila:\n" . mb_substr( $raw_content, 0, 2000 ) . "\n\n" . "Odpowiedz TYLKO w formacie JSON bez żadnych dodatkowych wyjaśnień:\n" . '{"task_name": "temat zadania", "task_text": "opis zadania"}'; $payload = [ 'model' => $model, 'messages' => [ [ 'role' => 'system', 'content' => 'Jesteś asystentem do przetwarzania emaili. Odpowiadasz TYLKO w formacie JSON.' ], [ 'role' => 'user', 'content' => $prompt ] ], 'temperature' => 0.3, 'max_tokens' => 500 ]; $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 ); $http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE ); curl_close( $ch ); if ( $http_code !== 200 || !$response ) return null; $data = json_decode( $response, true ); if ( !isset( $data['choices'][0]['message']['content'] ) ) return null; $content = trim( $data['choices'][0]['message']['content'] ); // Usuń markdown code block jeśli jest $content = preg_replace( '/```json\s*|\s*```/', '', $content ); $content = trim( $content ); $parsed = json_decode( $content, true ); if ( !is_array( $parsed ) || !isset( $parsed['task_name'] ) || !isset( $parsed['task_text'] ) ) return null; $task_name = trim( (string)$parsed['task_name'] ); $task_text = trim( (string)$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 ]; } }