netgescon-day0/app/Services/Posta/ImapMailboxService.php

299 lines
12 KiB
PHP

<?php
namespace App\Services\Posta;
use App\Models\CommunicationMessage;
use App\Models\Persona;
use App\Models\PersonaUnitaRelazione;
use App\Models\Stabile;
use App\Models\Ticket;
use App\Models\UnitaImmobiliare;
use App\Services\TenantArchivePathService;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class ImapMailboxService
{
public function __construct(
private readonly ImapClient $imapClient,
private readonly EmlParser $emlParser,
private readonly TenantArchivePathService $pathService
) {}
public function testMailbox(array $config): array
{
$host = trim((string) ($config['host'] ?? ''));
$port = (int) ($config['port'] ?? 993);
$username = trim((string) ($config['username'] ?? ($config['email'] ?? '')));
$password = (string) ($config['password'] ?? '');
$encryption = trim((string) ($config['encryption'] ?? 'ssl')) ?: 'ssl';
if ($host === '' || $username === '') {
return ['success' => 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;
}
/**
* 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;
}
}