417 lines
16 KiB
PHP
Executable File
417 lines
16 KiB
PHP
Executable File
<?php
|
|
namespace App\Services\Support;
|
|
|
|
use App\Models\CommunicationMessage;
|
|
use App\Models\Documento;
|
|
use App\Models\Stabile;
|
|
use App\Models\Ticket;
|
|
use App\Models\TicketAttachment;
|
|
use App\Support\GoogleAccountStore;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use RuntimeException;
|
|
|
|
class GmailTicketImportService
|
|
{
|
|
public function __construct(
|
|
private readonly GoogleAccountStore $googleAccountStore,
|
|
private readonly TicketAttachmentUploadService $ticketAttachmentUploadService,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $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<string, string>
|
|
*/
|
|
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<string, mixed> $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<string, mixed> $part
|
|
* @param array<int, string> $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<string, mixed> $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<string, mixed> $payload
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
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<string, mixed> $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<string, mixed>
|
|
*/
|
|
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;
|
|
}
|
|
}
|
|
}
|