$mailbox * @return array{mailbox:string,imported:int,skipped:int,attachments:int} */ public function importMailbox(Stabile $stabile, array $mailbox, int $maxMessages = 10): array { $amministratore = $stabile->amministratore; if (! $amministratore) { throw new RuntimeException('Amministratore non disponibile per lo stabile #' . $stabile->id . '.'); } $openingUserId = (int) ($amministratore->user_id ?? 0); if ($openingUserId <= 0) { throw new RuntimeException('Utente amministratore non disponibile per aprire i ticket dello stabile #' . $stabile->id . '.'); } $accountKey = trim((string) ($mailbox['google_account_key'] ?? '')); $token = $this->googleAccountStore->resolveAccessToken($amministratore, $accountKey, false); if ($token === null || $token === '') { throw new RuntimeException('Token Google non disponibile per la casella Gmail collegata allo stabile #' . $stabile->id . '.'); } $query = trim((string) ($mailbox['gmail_query'] ?? '')); $response = Http::withToken($token) ->acceptJson() ->get('https://gmail.googleapis.com/gmail/v1/users/me/messages', array_filter([ 'maxResults' => max(1, min($maxMessages, 50)), 'q' => $query !== '' ? $query : null, ], static fn ($value) => $value !== null)); if (! $response->successful()) { throw new RuntimeException('Gmail API non disponibile: ' . $response->body()); } $imported = 0; $skipped = 0; $attachments = 0; $messages = (array) Arr::get($response->json(), 'messages', []); foreach ($messages as $row) { $gmailId = trim((string) ($row['id'] ?? '')); if ($gmailId === '') { continue; } $message = $this->fetchMessage($token, $gmailId, 'full'); $headers = $this->extractHeaders((array) Arr::get($message, 'payload.headers', [])); $headerMessageId = trim((string) ($headers['message-id'] ?? '')); $externalId = $headerMessageId !== '' ? $headerMessageId : $gmailId; if ($this->alreadyImported($externalId, $gmailId)) { $skipped++; continue; } $bodyText = $this->extractBodyText((array) Arr::get($message, 'payload', [])); $subject = trim((string) ($headers['subject'] ?? '')); $from = trim((string) ($headers['from'] ?? '')); $to = trim((string) ($headers['to'] ?? '')); $date = $headers['date'] ?? null; $ticket = Ticket::query()->create([ 'stabile_id' => (int) $stabile->id, 'aperto_da_user_id' => $openingUserId, 'titolo' => Str::limit($subject !== '' ? $subject : ('Email importata da ' . ($from !== '' ? $from : 'Gmail')), 255), 'descrizione' => $this->buildTicketDescription($stabile, $mailbox, $subject, $from, $to, $date, $bodyText), 'categoria_ticket_id' => null, 'luogo_intervento' => 'Casella email stabile', 'data_apertura' => $this->normalizeDate($date) ?? now(), 'stato' => 'Aperto', 'priorita' => 'Media', ]); $rawMessage = $this->fetchMessage($token, $gmailId, 'raw'); $rawEml = $this->decodeBase64Url((string) Arr::get($rawMessage, 'raw', '')); $documento = $this->storeRawEmailDocumento($ticket, $mailbox, $subject, $from, $to, $date, $rawEml, $gmailId, $openingUserId); $ticket->messages()->create([ 'user_id' => $openingUserId, 'messaggio' => Str::limit($bodyText !== '' ? $bodyText : ($subject !== '' ? $subject : 'Email importata da Gmail'), 4000), 'canale' => 'email', 'direzione' => 'inbound', 'oggetto' => $subject !== '' ? $subject : null, 'email_mittente' => $from !== '' ? $from : null, 'email_destinatario' => $to !== '' ? $to : null, 'external_message_id' => $externalId, 'inviato_il' => $this->normalizeDate($date), 'eml_documento_id' => $documento->id, 'metadata' => [ 'gmail_message_id' => $gmailId, 'gmail_thread_id' => Arr::get($message, 'threadId'), 'gmail_mailbox_email' => $mailbox['email'] ?? null, 'google_account_key' => $mailbox['google_account_key'] ?? null, ], ]); CommunicationMessage::query()->create([ 'channel' => 'email', 'direction' => 'inbound', 'external_message_id' => $externalId, 'stabile_id' => (int) $stabile->id, 'sender_name' => $from !== '' ? $from : null, 'message_text' => Str::limit($bodyText !== '' ? $bodyText : ($subject !== '' ? $subject : 'Email importata da Gmail'), 4000), 'ticket_id' => (int) $ticket->id, 'status' => 'linked_to_ticket', 'received_at' => $this->normalizeDate($date), 'metadata' => [ 'gmail_message_id' => $gmailId, 'gmail_thread_id' => Arr::get($message, 'threadId'), 'gmail_mailbox_email' => $mailbox['email'] ?? null, 'google_account_key' => $mailbox['google_account_key'] ?? null, 'subject' => $subject, 'to' => $to, 'documento_id' => (int) $documento->id, ], ]); $attachments += $this->storeAttachments($token, $gmailId, $ticket, (array) Arr::get($message, 'payload', []), $subject, $openingUserId); $imported++; } return [ 'mailbox' => (string) ($mailbox['label'] ?? $mailbox['email'] ?? 'casella-gmail'), 'imported' => $imported, 'skipped' => $skipped, 'attachments' => $attachments, ]; } /** * @return array */ private function extractHeaders(array $headers): array { $result = []; foreach ($headers as $header) { $name = strtolower(trim((string) ($header['name'] ?? ''))); if ($name === '') { continue; } $result[$name] = trim((string) ($header['value'] ?? '')); } return $result; } /** * @param array $payload */ private function extractBodyText(array $payload): string { $parts = []; $this->collectTextParts($payload, $parts); $plain = trim(implode("\n\n", array_filter($parts))); if ($plain !== '') { return Str::limit($plain, 3500); } return trim((string) Arr::get($payload, 'body.data', '')) !== '' ? Str::limit($this->decodeBase64Url((string) Arr::get($payload, 'body.data', '')), 3500) : ''; } /** * @param array $part * @param array $collector */ private function collectTextParts(array $part, array &$collector): void { $mimeType = strtolower(trim((string) ($part['mimeType'] ?? ''))); if ($mimeType === 'text/plain') { $text = trim($this->decodeBase64Url((string) Arr::get($part, 'body.data', ''))); if ($text !== '') { $collector[] = $text; } } foreach ((array) ($part['parts'] ?? []) as $child) { if (is_array($child)) { $this->collectTextParts($child, $collector); } } } /** * @param array $payload */ private function storeAttachments(string $token, string $gmailId, Ticket $ticket, array $payload, string $subject, int $userId): int { $count = 0; foreach ($this->flattenAttachmentParts($payload) as $part) { $fileName = trim((string) ($part['filename'] ?? '')); if ($fileName === '') { continue; } $data = trim((string) Arr::get($part, 'body.data', '')); if ($data === '') { $attachmentId = trim((string) Arr::get($part, 'body.attachmentId', '')); if ($attachmentId !== '') { $response = Http::withToken($token) ->acceptJson() ->get('https://gmail.googleapis.com/gmail/v1/users/me/messages/' . rawurlencode($gmailId) . '/attachments/' . rawurlencode($attachmentId)); if ($response->successful()) { $data = trim((string) Arr::get($response->json(), 'data', '')); } } } if ($data === '') { continue; } $binary = $this->decodeBase64Url($data); if ($binary === '') { continue; } $stored = $this->ticketAttachmentUploadService->storeRawContents( $binary, 'ticket-email/' . $ticket->id, $fileName, [ 'mime' => (string) ($part['mimeType'] ?? ''), 'optimize_image' => false, 'display_name' => $fileName, 'stored_basename' => pathinfo($fileName, PATHINFO_FILENAME), ] ); TicketAttachment::query()->create([ 'ticket_id' => (int) $ticket->id, 'ticket_update_id' => null, 'user_id' => $userId, 'file_path' => $stored['path'], 'original_file_name' => $stored['original_name'], 'mime_type' => $stored['mime'], 'size' => $stored['size'], 'description' => Str::limit('Allegato email importata' . ($subject !== '' ? ': ' . $subject : ''), 255), ]); $count++; } return $count; } /** * @param array $payload * @return array> */ private function flattenAttachmentParts(array $payload): array { $parts = []; foreach ((array) ($payload['parts'] ?? []) as $part) { if (! is_array($part)) { continue; } if (trim((string) ($part['filename'] ?? '')) !== '') { $parts[] = $part; } $parts = array_merge($parts, $this->flattenAttachmentParts($part)); } return $parts; } private function alreadyImported(string $externalId, string $gmailId): bool { return CommunicationMessage::query() ->whereIn('external_message_id', array_values(array_filter([$externalId, $gmailId]))) ->exists(); } /** * @param array $mailbox */ private function buildTicketDescription(Stabile $stabile, array $mailbox, string $subject, string $from, string $to, mixed $date, string $bodyText): string { $lines = [ 'Email importata automaticamente dalla casella stabile.', 'Stabile: ' . ($stabile->denominazione ?: ('#' . $stabile->id)), ]; $mailboxLabel = trim((string) ($mailbox['label'] ?? '')); $mailboxEmail = trim((string) ($mailbox['email'] ?? '')); if ($mailboxLabel !== '' || $mailboxEmail !== '') { $lines[] = 'Casella: ' . trim($mailboxLabel . ($mailboxEmail !== '' ? ' <' . $mailboxEmail . '>' : '')); } if ($subject !== '') { $lines[] = 'Oggetto: ' . $subject; } if ($from !== '') { $lines[] = 'Mittente: ' . $from; } if ($to !== '') { $lines[] = 'Destinatario: ' . $to; } if ($date) { $lines[] = 'Data email: ' . (string) $date; } if ($bodyText !== '') { $lines[] = ''; $lines[] = 'Corpo messaggio:'; $lines[] = $bodyText; } return trim(implode("\n", $lines)); } private function storeRawEmailDocumento(Ticket $ticket, array $mailbox, string $subject, string $from, string $to, mixed $date, string $rawEml, string $gmailId, int $userId): Documento { $fileName = 'gmail-' . $gmailId . '.eml'; $path = 'documenti/ticket-email/' . $ticket->id . '/' . $fileName; Storage::disk('public')->put($path, $rawEml); return Documento::query()->create([ 'documentable_id' => $ticket->id, 'documentable_type' => Ticket::class, 'stabile_id' => $ticket->stabile_id, 'utente_id' => $userId, 'nome_file' => $fileName, 'path_file' => $path, 'percorso_file' => $path, 'tipo_documento' => 'Email EML', 'tipologia' => 'comunicazione', 'nome' => $subject !== '' ? $subject : ('Email Gmail ticket #' . $ticket->id), 'mime_type' => 'message/rfc822', 'estensione' => 'eml', 'dimensione_file' => strlen($rawEml), 'data_documento' => $this->normalizeDate($date), 'data_upload' => now(), 'descrizione' => 'Email importata automaticamente da ' . trim((string) ($mailbox['email'] ?? 'Gmail')), 'tags' => 'email,ticket,gmail,pec', 'note' => 'Import automatico Gmail per ticket #' . $ticket->id . ' da ' . ($from !== '' ? $from : 'mittente non disponibile'), ]); } /** * @return array */ private function fetchMessage(string $token, string $gmailId, string $format): array { $response = Http::withToken($token) ->acceptJson() ->get('https://gmail.googleapis.com/gmail/v1/users/me/messages/' . rawurlencode($gmailId), [ 'format' => $format, ]); if (! $response->successful()) { throw new RuntimeException('Impossibile leggere il messaggio Gmail ' . $gmailId . '.'); } return (array) $response->json(); } private function decodeBase64Url(string $value): string { $normalized = strtr($value, '-_', '+/'); $padding = strlen($normalized) % 4; if ($padding > 0) { $normalized .= str_repeat('=', 4 - $padding); } $decoded = base64_decode($normalized, true); return is_string($decoded) ? $decoded : ''; } private function normalizeDate(mixed $value): mixed { $raw = trim((string) $value); if ($raw === '') { return null; } try { return Carbon::parse($raw); } catch (\Throwable) { return null; } } }