false, 'message' => 'Host e Username sono obbligatori per testare la connessione IMAP.']; } return $this->imapClient->testConnection($host, $port, $username, $password, $encryption); } /** * Scarica e memorizza le email/PEC via IMAP in formato .EML e popola CommunicationMessage e Ticket. * * @param Stabile $stabile * @param array $mailbox * @param int $maxMessages * @return array{imported: int, skipped: int, errors: int, messages: array} */ public function fetchMailbox(Stabile $stabile, array $mailbox, int $maxMessages = 50): array { $host = trim((string) ($mailbox['host'] ?? '')); $port = (int) ($mailbox['port'] ?? 993); $username = trim((string) ($mailbox['username'] ?? ($mailbox['email'] ?? ''))); $password = (string) ($mailbox['password'] ?? ''); $encryption = trim((string) ($mailbox['encryption'] ?? 'ssl')) ?: 'ssl'; $folder = trim((string) ($mailbox['folder'] ?? 'INBOX')) ?: 'INBOX'; $isPec = strtolower(trim((string) ($mailbox['tipo'] ?? ''))) === 'pec'; $createTicket = (bool) ($mailbox['crea_ticket_automatico'] ?? true); if ($host === '' || $username === '') { return [ 'imported' => 0, 'skipped' => 0, 'errors' => 1, 'message' => 'Parametri IMAP incompleti (Host / Username mancanti)', 'messages' => [], ]; } $stats = ['imported' => 0, 'skipped' => 0, 'errors' => 0, 'messages' => []]; try { $this->imapClient->connect($host, $port, $username, $password, $encryption); $this->imapClient->selectFolder($folder); $msgIds = $this->imapClient->search('ALL'); // Prendi i messaggi più recenti fino a $maxMessages $recentIds = array_slice(array_reverse($msgIds), 0, $maxMessages); foreach ($recentIds as $msgId) { try { $rawEml = $this->imapClient->fetchRawEml($msgId); if (trim($rawEml) === '') { continue; } $res = $this->ingestEmlString($rawEml, $stabile, $mailbox, $isPec, $createTicket); if ($res['status'] === 'imported') { $stats['imported']++; $stats['messages'][] = $res; } elseif ($res['status'] === 'duplicate') { $stats['skipped']++; } else { $stats['errors']++; } } catch (\Throwable $e) { $stats['errors']++; } } $this->imapClient->disconnect(); } catch (\Throwable $e) { $stats['errors']++; $stats['error_message'] = $e->getMessage(); } return $stats; } /** * Scarica e memorizza le email/PEC dello studio amministratore in formato .EML * * @param \App\Models\Amministratore $amministratore * @param array $mailbox * @param int $maxMessages * @return array{imported: int, skipped: int, errors: int, messages: array} */ public function fetchStudioMailbox(\App\Models\Amministratore $amministratore, array $mailbox, int $maxMessages = 50): array { $host = trim((string) ($mailbox['host'] ?? '')); $port = (int) ($mailbox['port'] ?? 993); $username = trim((string) ($mailbox['username'] ?? ($mailbox['email'] ?? ''))); $password = (string) ($mailbox['password'] ?? ''); $encryption = trim((string) ($mailbox['encryption'] ?? 'ssl')) ?: 'ssl'; $folder = trim((string) ($mailbox['folder'] ?? 'INBOX')) ?: 'INBOX'; $isPec = strtolower(trim((string) ($mailbox['tipo'] ?? ''))) === 'pec'; if ($host === '' || $username === '') { return [ 'imported' => 0, 'skipped' => 0, 'errors' => 1, 'message' => 'Parametri IMAP studio incompleti', 'messages' => [], ]; } $stats = ['imported' => 0, 'skipped' => 0, 'errors' => 0, 'messages' => []]; try { $this->imapClient->connect($host, $port, $username, $password, $encryption); $this->imapClient->selectFolder($folder); $msgIds = $this->imapClient->search('ALL'); $recentIds = array_slice(array_reverse($msgIds), 0, $maxMessages); foreach ($recentIds as $msgId) { try { $rawEml = $this->imapClient->fetchRawEml($msgId); if (trim($rawEml) === '') { continue; } $res = $this->ingestStudioEmlString($rawEml, $amministratore, $mailbox, $isPec); if ($res['status'] === 'imported') { $stats['imported']++; $stats['messages'][] = $res; } elseif ($res['status'] === 'duplicate') { $stats['skipped']++; } else { $stats['errors']++; } } catch (\Throwable $e) { $stats['errors']++; } } $this->imapClient->disconnect(); } catch (\Throwable $e) { $stats['errors']++; $stats['error_message'] = $e->getMessage(); } return $stats; } /** * Ingesta un messaggio EML dello studio amministratore */ public function ingestStudioEmlString( string $rawEml, \App\Models\Amministratore $amministratore, array $mailboxConfig = [], bool $forcePec = false ): array { $parsed = $this->emlParser->parse($rawEml); $messageId = $parsed['message_id'] ?: ('studio_' . md5($rawEml)); $existing = CommunicationMessage::whereNull('stabile_id') ->where(function ($q) use ($messageId) { $q->where('external_message_id', $messageId) ->orWhere('metadata->message_id', $messageId); }) ->first(); if ($existing) { return ['status' => 'duplicate', 'id' => $existing->id]; } $channel = ($forcePec || $parsed['is_pec']) ? 'pec' : 'email'; $year = $parsed['date'] ? $parsed['date']->format('Y') : date('Y'); $postaSubdir = $channel === 'pec' ? 'posta_pec' : 'posta_ordinaria'; $storageDir = $this->pathService->amministratoreAbsolutePath($amministratore, "studio/{$postaSubdir}/{$year}"); if (! is_dir($storageDir)) { @mkdir($storageDir, 0755, true); } $safeMsgId = preg_replace('/[^a-zA-Z0-9_-]/', '_', $messageId); $emlFilename = "{$safeMsgId}.eml"; $emlFullPath = "{$storageDir}/{$emlFilename}"; @file_put_contents($emlFullPath, $rawEml); $savedAttachments = []; if (! empty($parsed['attachments'])) { $attachmentsDir = "{$storageDir}/allegati/{$safeMsgId}"; if (! is_dir($attachmentsDir)) { @mkdir($attachmentsDir, 0755, true); } foreach ($parsed['attachments'] as $idx => $att) { $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', (string) $att['filename']); if ($safeName === '') { $safeName = "allegato_{$idx}.dat"; } $attPath = "{$attachmentsDir}/{$safeName}"; @file_put_contents($attPath, $att['content']); $savedAttachments[] = [ 'filename' => $att['filename'], 'path' => $attPath, 'content_type' => $att['content_type'], 'size' => $att['size'], 'is_inline' => $att['is_inline'], ]; } } $senderDisplay = $parsed['from']['name'] ? "{$parsed['from']['name']} <{$parsed['from']['email']}>" : $parsed['from']['email']; $comm = CommunicationMessage::create([ 'channel' => $channel, 'direction' => 'inbound', 'external_message_id' => $messageId, 'stabile_id' => null, 'sender_name' => $senderDisplay, 'phone_number' => null, 'message_text' => $parsed['body_text'] ?: strip_tags((string) $parsed['body_html']), 'attachments' => $savedAttachments, 'ticket_id' => null, 'status' => 'received', 'received_at' => $parsed['date'] ?: now(), 'metadata' => [ 'subject' => $parsed['subject'], 'sender_email' => $parsed['from']['email'], 'sender_name' => $parsed['from']['name'], 'to' => $parsed['to'], 'cc' => $parsed['cc'], 'body_html' => $parsed['body_html'], 'eml_path' => $emlFullPath, 'is_pec' => $channel === 'pec', 'is_studio' => true, 'mailbox_label' => $mailboxConfig['label'] ?? 'Studio', 'mailbox_email' => $mailboxConfig['email'] ?? '', ], ]); return [ 'status' => 'imported', 'id' => $comm->id, 'channel' => $channel, 'subject' => $parsed['subject'], 'from' => $senderDisplay, 'date' => $parsed['date']?->format('d/m/Y H:i'), 'has_attachments' => count($savedAttachments) > 0, ]; } /** * Ingesta un messaggio da stringa EML grezza */ public function ingestEmlString( string $rawEml, Stabile $stabile, array $mailboxConfig = [], bool $forcePec = false, bool $createTicket = true ): array { $parsed = $this->emlParser->parse($rawEml); $messageId = $parsed['message_id'] ?: ('local_' . md5($rawEml)); // Verifica duplicati su CommunicationMessage $existing = CommunicationMessage::where('stabile_id', $stabile->id) ->where(function ($q) use ($messageId) { $q->where('external_message_id', $messageId) ->orWhere('metadata->message_id', $messageId); }) ->first(); if ($existing) { return ['status' => 'duplicate', 'id' => $existing->id]; } $channel = ($forcePec || $parsed['is_pec']) ? 'pec' : 'email'; $year = $parsed['date'] ? $parsed['date']->format('Y') : date('Y'); // Cartelle dedicate di archiviazione .EML $postaSubdir = $channel === 'pec' ? 'posta_pec' : 'posta_ordinaria'; $storageDir = $this->pathService->stabileAbsolutePath($stabile, "{$postaSubdir}/{$year}"); if (! is_dir($storageDir)) { @mkdir($storageDir, 0755, true); } $safeMsgId = preg_replace('/[^a-zA-Z0-9_-]/', '_', $messageId); $emlFilename = "{$safeMsgId}.eml"; $emlFullPath = "{$storageDir}/{$emlFilename}"; @file_put_contents($emlFullPath, $rawEml); // Salva gli allegati estratti $savedAttachments = []; if (! empty($parsed['attachments'])) { $attachmentsDir = "{$storageDir}/allegati/{$safeMsgId}"; if (! is_dir($attachmentsDir)) { @mkdir($attachmentsDir, 0755, true); } foreach ($parsed['attachments'] as $idx => $att) { $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', (string) $att['filename']); if ($safeName === '') { $safeName = "allegato_{$idx}.dat"; } $attPath = "{$attachmentsDir}/{$safeName}"; @file_put_contents($attPath, $att['content']); $savedAttachments[] = [ 'filename' => $att['filename'], 'path' => $attPath, 'content_type' => $att['content_type'], 'size' => $att['size'], 'is_inline' => $att['is_inline'], ]; } } // Risoluzione Unità Immobiliare associata al mittente $matchedUnita = $this->matchUnitaBySender($stabile, $parsed['from']['email']); // Creazione Ticket opzionale $ticketId = null; if ($createTicket) { try { $userId = \Illuminate\Support\Facades\Auth::id(); if (! $userId && $stabile->amministratore_id) { $userId = DB::table('amministratori')->where('id', $stabile->amministratore_id)->value('user_id'); } if (! $userId) { $userId = \App\Models\User::query()->value('id') ?: 1; } $ticket = Ticket::create([ 'stabile_id' => $stabile->id, 'unita_immobiliare_id' => $matchedUnita?->id, 'aperto_da_user_id' => (int) $userId, 'titolo' => Str::limit($parsed['subject'], 180, '...'), 'descrizione' => Str::limit($parsed['body_text'] ?: strip_tags((string) $parsed['body_html']), 1000), 'priorita' => $channel === 'pec' ? 'Alta' : 'Media', 'stato' => 'Aperto', 'data_apertura' => now(), ]); $ticketId = $ticket->id; } catch (\Throwable $e) { Log::warning('ImapMailboxService: Ticket creation failed: ' . $e->getMessage()); } } // Creazione CommunicationMessage $senderDisplay = $parsed['from']['name'] ? "{$parsed['from']['name']} <{$parsed['from']['email']}>" : $parsed['from']['email']; $comm = CommunicationMessage::create([ 'channel' => $channel, 'direction' => 'inbound', 'external_message_id' => $messageId, 'stabile_id' => $stabile->id, 'sender_name' => $senderDisplay, 'phone_number' => null, 'message_text' => $parsed['body_text'] ?: strip_tags((string) $parsed['body_html']), 'attachments' => $savedAttachments, 'ticket_id' => $ticketId, 'status' => 'received', 'received_at' => $parsed['date'] ?: now(), 'metadata' => [ 'subject' => $parsed['subject'], 'sender_email' => $parsed['from']['email'], 'sender_name' => $parsed['from']['name'], 'to' => $parsed['to'], 'cc' => $parsed['cc'], 'body_html' => $parsed['body_html'], 'eml_path' => $emlFullPath, 'is_pec' => $channel === 'pec', 'unita_immobiliare_id' => $matchedUnita?->id, 'codice_unita' => $matchedUnita?->codice_unita, 'mailbox_label' => $mailboxConfig['label'] ?? '', 'mailbox_email' => $mailboxConfig['email'] ?? '', ], ]); return [ 'status' => 'imported', 'id' => $comm->id, 'channel' => $channel, 'subject' => $parsed['subject'], 'from' => $senderDisplay, 'date' => $parsed['date']?->format('d/m/Y H:i'), 'ticket_id' => $ticketId, 'unita_id' => $matchedUnita?->id, 'has_attachments' => count($savedAttachments) > 0, ]; } private function matchUnitaBySender(Stabile $stabile, string $senderEmail): ?UnitaImmobiliare { $senderEmail = strtolower(trim($senderEmail)); if ($senderEmail === '') { return null; } // Cerca persone associate all'email o contatti $personaIds = Persona::where('email_principale', $senderEmail) ->orWhere('email_secondaria', $senderEmail) ->orWhere('pec', $senderEmail) ->pluck('id') ->toArray(); if (empty($personaIds)) { // Cerca in rubrica o recapiti if (DB::getSchemaBuilder()->hasTable('rubrica_universale')) { $rubricaIds = DB::table('rubrica_universale') ->where(function ($q) use ($senderEmail) { $q->where('email', $senderEmail) ->orWhere('email_secondaria', $senderEmail) ->orWhere('pec', $senderEmail); }) ->pluck('id') ->toArray(); if (! empty($rubricaIds) && DB::getSchemaBuilder()->hasTable('rubrica_ruoli')) { $unitaId = DB::table('rubrica_ruoli') ->where('stabile_id', $stabile->id) ->whereIn('rubrica_id', $rubricaIds) ->whereNotNull('unita_immobiliare_id') ->value('unita_immobiliare_id'); if ($unitaId) { return UnitaImmobiliare::find($unitaId); } } } return null; } $unitaId = PersonaUnitaRelazione::whereIn('persona_id', $personaIds) ->whereHas('unita', fn($q) => $q->where('stabile_id', $stabile->id)) ->value('unita_id'); return $unitaId ? UnitaImmobiliare::find($unitaId) : null; } }