Files
glastree/app/Http/Controllers/EmailController.php
T
2026-06-07 17:27:00 +02:00

716 lines
27 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\EmailFolder;
use App\Models\EmailMessage;
use App\Models\EmailAttachment;
use App\Models\EmailSetting;
use App\Models\SenderAccount;
use App\Models\MailingList;
use App\Models\Gruppo;
use App\Models\Individuo;
use App\Models\Documento;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class EmailController extends Controller
{
public function index(Request $request, $folder = 'inbox')
{
$this->authorizeRead('email');
if ($folder === 'compose' || $folder === 'nuovo') {
return $this->compose($request);
}
$folderModel = EmailFolder::where('type', $folder)->first();
if (!$folderModel) {
$folderModel = EmailFolder::where('type', 'inbox')->first();
if (!$folderModel) {
$this->ensureFoldersExist();
$folderModel = EmailFolder::where('type', 'inbox')->first();
}
}
$query = EmailMessage::where('email_folder_id', $folderModel->id);
if ($request->search) {
$query->where(function ($q) use ($request) {
$q->where('subject', 'like', '%' . $request->search . '%')
->orWhere('from_email', 'like', '%' . $request->search . '%')
->orWhere('to_email', 'like', '%' . $request->search . '%')
->orWhere('body_text', 'like', '%' . $request->search . '%');
});
}
$sortField = $request->sort ?? 'received_at';
$sortDir = $request->direction ?? 'desc';
$allowedSorts = ['subject', 'received_at', 'sent_at', 'from_email', 'to_email'];
if (!in_array($sortField, $allowedSorts)) {
$sortField = 'received_at';
}
if (!in_array($sortDir, ['asc', 'desc'])) {
$sortDir = 'desc';
}
$query->orderBy($sortField === 'sent_at' ? 'received_at' : $sortField, $sortDir);
$perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20;
$messages = $query->paginate($perPage)->withQueryString();
$folders = EmailFolder::orderBy('sort_order')->get();
$mailingLists = MailingList::where('attiva', true)->get();
$gruppi = Gruppo::orderBy('nome')->get();
$emailSettings = EmailSetting::getActive();
$syncInterval = $emailSettings ? $emailSettings->sync_interval_minutes : 0;
return view('email.index', compact('messages', 'folders', 'folder', 'folderModel', 'mailingLists', 'gruppi', 'syncInterval'));
}
public function show($folder, $id)
{
$this->authorizeRead('email');
$message = EmailMessage::with('attachments')->findOrFail($id);
if (!$message->is_read) {
$message->update(['is_read' => true]);
}
$folders = EmailFolder::orderBy('sort_order')->get();
return view('email.show', compact('message', 'folders', 'folder'));
}
public function compose(Request $request)
{
$this->authorizeWrite('email');
$folders = EmailFolder::orderBy('sort_order')->get();
$mailingLists = MailingList::where('attiva', true)->get();
$gruppi = Gruppo::orderBy('nome')->get();
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
$senderAccounts = SenderAccount::active()->get();
$prefill = [];
if ($request->reply_to) {
$original = EmailMessage::find($request->reply_to);
if ($original) {
$prefill['to'] = $original->from_email;
$prefill['subject'] = 'Re: ' . $original->subject;
$prefill['body'] = "\n\n--- Messaggio originale ---\n" . ($original->body_text ?? $original->body_html);
}
}
if ($request->forward) {
$original = EmailMessage::find($request->forward);
if ($original) {
$prefill['subject'] = 'Fwd: ' . $original->subject;
$prefill['body'] = "\n\n--- Messaggio inoltrato ---\n" . ($original->body_text ?? $original->body_html);
}
}
return view('email.compose', compact('folders', 'mailingLists', 'gruppi', 'individui', 'prefill', 'senderAccounts'));
}
public function send(Request $request)
{
$this->authorizeWrite('email');
$rules = [
'subject' => 'required',
'body' => 'required',
];
if ($request->destinatario_tipo === 'manuale' || !$request->destinatario_tipo) {
$rules['to'] = 'required';
}
$validated = $request->validate($rules);
$recipients = $this->resolveRecipients($request);
\Illuminate\Support\Facades\Log::info('Email send attempt', [
'recipients' => $recipients,
'recipients_count' => count($recipients),
'subject' => $validated['subject'],
'destinatario_tipo' => $request->destinatario_tipo,
'mittente_id' => $request->mittente_id,
]);
if (empty($recipients)) {
return back()->with('error', 'Nessun destinatario trovato. Seleziona almeno un destinatario dal menu a tendina o inserisci un indirizzo email manuale.')->withInput();
}
$useSender = $request->filled('mittente_id') && $request->mittente_id > 0;
if ($useSender) {
return $this->sendWithSender($request, $validated, $recipients);
}
$settings = EmailSetting::getActive();
if (!$settings) {
return back()->with('error', 'Account email non configurato.');
}
$imapSuccess = false;
$imapError = null;
$attachments = $this->processAttachments($request);
foreach ($recipients as $recipient) {
try {
$bodyWithSig = $this->appendSignature($validated['body'], $settings->getSignature(), $settings->signature_enabled ?? false);
$this->sendViaImap($settings, $recipient, $validated['subject'], $bodyWithSig, $request->cc, $attachments);
$imapSuccess = true;
} catch (\Exception $e) {
$imapError = $e->getMessage();
\Illuminate\Support\Facades\Log::error('SMTP send error', ['error' => $e->getMessage()]);
}
}
try {
$bodyWithSig = $this->appendSignature($validated['body'], $settings->getSignature(), $settings->signature_enabled ?? false);
$this->storeSentMessage($settings, $recipients, $validated['subject'], $bodyWithSig, $request->cc);
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Store sent message error', ['error' => $e->getMessage()]);
return back()->with('error', 'Errore nel salvataggio: ' . $e->getMessage())->withInput();
}
if (!$imapSuccess && $imapError) {
return redirect()->route('email.index', 'sent')->with('warning', 'Email salvata in Invio ma non inviata via SMTP: ' . $imapError);
}
return redirect()->route('email.index', 'sent')->with('success', 'Email inviata con successo.');
}
private function sendWithSender(Request $request, array $validated, array $recipients)
{
$sender = SenderAccount::findOrFail($request->mittente_id);
$attachmentPaths = $this->resolveAttachmentPaths($request);
$ok = 0;
$errors = [];
foreach ($recipients as $recipient) {
try {
$sender->sendEmail($recipient, $validated['subject'], $validated['body'], $attachmentPaths);
$ok++;
} catch (\Exception $e) {
$errors[] = $recipient . ': ' . $e->getMessage();
}
}
if ($sender->verify_email) {
$reportSubject = '✅ Invio: ' . $validated['subject'] . ' - ' . $sender->email_address;
$reportBody = "Report invio mittente: {$sender->email_address}\n"
. "Destinatari totali: " . count($recipients) . "\n"
. "Inviate con successo: {$ok}\n"
. "Fallite: " . count($errors) . "\n";
if (!empty($errors)) {
$reportBody .= "\nDettaglio errori:\n" . implode("\n", $errors);
}
$sender->sendReport($reportSubject, $reportBody);
}
if ($ok === 0 && !empty($errors)) {
return redirect()->route('email.index', 'sent')->with('error', 'Invio fallito: ' . implode('; ', $errors));
}
$message = $ok . ' email inviate con successo tramite "' . $sender->email_address . '".';
if (!empty($errors)) {
$message .= ' ' . count($errors) . ' fallite.';
}
return redirect()->route('email.index', 'sent')->with('success', $message);
}
public function storeDraft(Request $request)
{
$this->authorizeWrite('email');
$settings = EmailSetting::getActive();
$folder = EmailFolder::where('type', 'drafts')->first();
$message = EmailMessage::create([
'email_folder_id' => $folder->id,
'subject' => $request->subject ?? '(senza oggetto)',
'body_text' => $request->body,
'to_email' => $request->to,
'is_draft' => true,
'from_email' => $settings->email_address ?? '',
'from_name' => $settings->email_name ?? '',
]);
return response()->json(['success' => true, 'draft_id' => $message->id]);
}
public function bulkAction(Request $request)
{
try {
$this->authorizeWrite('email');
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
return response()->json([
'success' => false,
'message' => 'Non hai i permessi per questa azione'
], 403);
}
$ids = $request->input('ids', []);
$action = $request->input('action');
if ($action === 'empty_trash') {
$trashFolder = EmailFolder::where('type', 'trash')->first();
if ($trashFolder) {
EmailMessage::where('email_folder_id', $trashFolder->id)->delete();
}
return response()->json(['success' => true]);
}
if (empty($ids)) {
return response()->json(['success' => false, 'message' => 'Nessuna email selezionata']);
}
$messages = EmailMessage::whereIn('id', $ids)->get();
switch ($action) {
case 'mark_read':
$messages->each->update(['is_read' => true]);
break;
case 'mark_unread':
$messages->each->update(['is_read' => false]);
break;
case 'star':
$messages->each->update(['is_starred' => true]);
break;
case 'unstar':
$messages->each->update(['is_starred' => false]);
break;
case 'move_trash':
$trashFolder = EmailFolder::where('type', 'trash')->first();
if ($trashFolder) {
$messages->each->update(['email_folder_id' => $trashFolder->id]);
}
break;
case 'delete_permanent':
$messages->each->delete();
break;
default:
return response()->json(['success' => false, 'message' => 'Azione non valida']);
}
return response()->json(['success' => true]);
}
public function markRead($id)
{
try {
$this->authorizeWrite('email');
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
return response()->json([
'success' => false,
'message' => 'Non hai i permessi per questa azione'
], 403);
}
$message = EmailMessage::findOrFail($id);
$message->update(['is_read' => true]);
return response()->json(['success' => true]);
}
public function toggleStar($id)
{
try {
$this->authorizeWrite('email');
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
return response()->json([
'success' => false,
'message' => 'Non hai i permessi per questa azione'
], 403);
}
$message = EmailMessage::findOrFail($id);
$message->update(['is_starred' => !$message->is_starred]);
return response()->json(['success' => true, 'is_starred' => $message->is_starred]);
}
public function destroy($id)
{
$this->authorizeDelete('email');
$message = EmailMessage::findOrFail($id);
$trashFolder = EmailFolder::where('type', 'trash')->first();
if ($trashFolder) {
$message->update(['is_trash' => true, 'email_folder_id' => $trashFolder->id]);
} else {
$message->delete();
}
return back()->with('success', 'Email spostata nel cestino.');
}
public function downloadAttachment($id)
{
$this->authorizeRead('email');
$attachment = EmailAttachment::findOrFail($id);
if (!Storage::exists($attachment->file_path)) {
abort(404);
}
return Storage::download($attachment->file_path, $attachment->filename);
}
public function saveAttachmentToDocumenti(Request $request, $id, $attachmentId)
{
$this->authorizeWrite('email');
$this->authorizeWrite('documenti');
$attachment = EmailAttachment::findOrFail($attachmentId);
$documento = Documento::create([
'nome_file' => $attachment->filename,
'file_path' => $attachment->file_path,
'tipo' => 'upload',
'tipologia' => 'email_attachment',
'mime_type' => $attachment->mime_type,
'dimensione' => $attachment->size,
'visibilita' => null,
'note' => 'Allegato email: ' . ($attachment->message->subject ?? ''),
]);
$attachment->update(['is_saved_to_documenti' => true, 'documenti_id' => $documento->id]);
return back()->with('success', 'Allegato salvato nei documenti.');
}
public function quickSync()
{
try {
$this->syncEmails();
return response()->json(['success' => true, 'message' => 'Sincronizzazione completata']);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()]);
}
}
public function syncEmails()
{
\Illuminate\Support\Facades\Log::info('Email sync started');
$settings = EmailSetting::getActive();
if (!$settings) {
\Illuminate\Support\Facades\Log::warning('Email sync aborted: no active email configuration');
throw new \Exception('Nessuna configurazione email attiva');
}
$this->ensureFoldersExist();
$mailbox = $this->connectImap($settings);
\Illuminate\Support\Facades\Log::info('IMAP connection established', ['host' => $settings->imap_host]);
$folderMappings = [
'INBOX' => 'inbox',
'Sent' => 'sent',
'[Gmail]/Sent Mail' => 'sent',
'Sent Mail' => 'sent',
'Drafts' => 'drafts',
'[Gmail]/Drafts' => 'drafts',
'Trash' => 'trash',
'[Gmail]/Trash' => 'trash',
];
$totalProcessed = 0;
$totalNew = 0;
foreach (array_keys($folderMappings) as $imapFolderName) {
try {
$folder = $mailbox->folders()->find($imapFolderName);
if (!$folder) {
\Illuminate\Support\Facades\Log::debug('IMAP folder not found', ['folder' => $imapFolderName]);
continue;
}
\Illuminate\Support\Facades\Log::info('Processing IMAP folder', ['folder' => $imapFolderName]);
$messages = $folder->messages()
->withHeaders()
->withBody()
->withBodyStructure()
->limit(100)
->get();
\Illuminate\Support\Facades\Log::info('Messages fetched from folder', ['folder' => $imapFolderName, 'count' => count($messages)]);
foreach ($messages as $imapMessage) {
$totalProcessed++;
$messageId = $imapMessage->messageId() ?? uniqid('msg_');
$existing = EmailMessage::where('message_id', $messageId)->first();
if ($existing) {
continue;
}
$this->processImapMessage($imapMessage, $imapFolderName);
$totalNew++;
}
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Error processing IMAP folder', ['folder' => $imapFolderName, 'error' => $e->getMessage()]);
continue;
}
}
\Illuminate\Support\Facades\Log::info('Email sync completed', [
'processed' => $totalProcessed,
'new' => $totalNew,
]);
return true;
}
private function resolveRecipients(Request $request)
{
$recipients = [];
if ($request->destinatario_tipo === 'individui' && $request->individui) {
$individui = Individuo::whereIn('id', $request->individui)->get();
foreach ($individui as $ind) {
$emails = $ind->contatti()->where('tipo', 'email')->pluck('valore')->toArray();
$recipients = array_merge($recipients, $emails);
}
} elseif ($request->destinatario_tipo === 'gruppo' && $request->gruppo_id) {
$gruppo = Gruppo::with('individui.contatti')->find($request->gruppo_id);
foreach ($gruppo->individui as $ind) {
$emails = $ind->contatti()->where('tipo', 'email')->pluck('valore')->toArray();
$recipients = array_merge($recipients, $emails);
}
} elseif ($request->destinatario_tipo === 'mailing_list' && $request->mailing_list_id) {
$list = MailingList::with('contatti.individuo')->find($request->mailing_list_id);
foreach ($list->contatti as $contact) {
if ($contact->opt_in && $contact->individuo) {
$emails = $contact->individuo->contatti()->where('tipo', 'email')->pluck('valore')->toArray();
$recipients = array_merge($recipients, $emails);
}
}
} elseif ($request->to) {
$recipients = is_array($request->to) ? $request->to : [$request->to];
}
return array_filter(array_unique($recipients));
}
private function appendSignature(string $body, ?string $signature, bool $enabled): string
{
if (empty($signature) || !$enabled) {
return $body;
}
return $body . "\n\n---\n" . $signature;
}
private function sendViaImap($settings, $to, $subject, $body, $cc = null, array $attachmentPaths = [])
{
\Illuminate\Support\Facades\Log::info('sendViaSMTP start', ['to' => $to, 'subject' => $subject]);
$smtpPassword = $settings->getDecryptedSmtpPassword() ?? $settings->getDecryptedPassword();
$encryption = $settings->smtp_encryption ?? 'tls';
$scheme = ($encryption === 'ssl') ? 'smtps' : 'smtp';
$dsn = sprintf(
'%s://%s:%s@%s:%d?encryption=%s',
$scheme,
rawurlencode($settings->smtp_username ?? $settings->email_address),
rawurlencode($smtpPassword),
$settings->smtp_host ?? 'smtp.gmail.com',
$settings->smtp_port ?? 587,
$encryption
);
try {
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
$fromName = $settings->email_name ?? '';
if (filter_var($fromName, FILTER_VALIDATE_EMAIL)) {
$fromName = 'Glastree';
} else {
$fromName = preg_replace('/[^\p{L}\p{N}\s]/u', '', $fromName);
$fromName = trim($fromName);
if (empty($fromName)) {
$fromName = 'Glastree';
}
}
$fromAddress = \Symfony\Component\Mime\Address::create($settings->email_address, $fromName);
$email = (new \Symfony\Component\Mime\Email())
->from($fromAddress)
->to($to)
->subject($subject)
->text($body);
if ($cc) {
$email->cc($cc);
}
foreach ($attachmentPaths as $attachmentPath) {
$email->attachFromPath($attachmentPath);
}
$mailer->send($email);
\Illuminate\Support\Facades\Log::info('Email sent via SMTP successfully');
return true;
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('SMTP send error', ['error' => $e->getMessage()]);
throw new \Exception('Errore invio SMTP: ' . $e->getMessage());
}
}
private function processAttachments(Request $request): array
{
$paths = [];
if ($request->hasFile('allegati')) {
foreach ($request->file('allegati') as $file) {
if ($file->isValid()) {
$paths[] = $file->getRealPath();
}
}
}
return $paths;
}
private function resolveAttachmentPaths(Request $request): array
{
$paths = [];
if ($request->hasFile('allegati')) {
foreach ($request->file('allegati') as $file) {
if ($file->isValid()) {
$paths[] = $file->getRealPath();
}
}
}
$documentIds = $request->input('documenti_selezionati');
if (!empty($documentIds)) {
$ids = is_array($documentIds) ? $documentIds : explode(',', $documentIds);
$docs = Documento::whereIn('id', $ids)->get();
foreach ($docs as $doc) {
if ($doc->file_path && \Illuminate\Support\Facades\Storage::exists($doc->file_path)) {
$paths[] = \Illuminate\Support\Facades\Storage::path($doc->file_path);
}
}
}
return $paths;
}
private function storeSentMessage($settings, $recipients, $subject, $body, $cc = null)
{
$sentFolder = EmailFolder::where('type', 'sent')->first();
$bodyWithSignature = $this->appendSignature($body, $settings->getSignature(), $settings->signature_enabled ?? false);
$message = EmailMessage::create([
'email_folder_id' => $sentFolder->id,
'subject' => $subject,
'body_text' => $bodyWithSignature,
'from_email' => $settings->email_address,
'from_name' => $settings->email_name,
'to_email' => implode(', ', $recipients),
'cc' => $cc,
'is_sent' => true,
'sent_at' => now(),
'is_read' => true,
]);
return $message;
}
private function connectImap($settings)
{
if (!$settings->imap_host || !$settings->imap_username) {
throw new \Exception('Configurazione IMAP non valida');
}
$encryption = match ($settings->imap_encryption) {
'ssl' => 'ssl',
'tls' => 'tls',
default => 'none',
};
return new \DirectoryTree\ImapEngine\Mailbox([
'host' => $settings->imap_host,
'port' => $settings->imap_port ?? 993,
'encryption' => $encryption,
'validate_cert' => false,
'username' => $settings->imap_username,
'password' => $settings->getDecryptedPassword(),
]);
}
private function processImapMessage($imapMessage, $imapFolder)
{
$folderType = match ($imapFolder) {
'INBOX', 'inbox' => 'inbox',
'Sent', 'sent', '[Gmail]/Sent Mail' => 'sent',
'Drafts', 'drafts', '[Gmail]/Drafts' => 'drafts',
'Trash', 'trash', '[Gmail]/Trash' => 'trash',
'Archive', 'archive', '[Gmail]/All Mail' => 'archive',
'Starred', 'starred', '[Gmail]/Starred' => 'starred',
default => 'inbox',
};
$folder = EmailFolder::where('type', $folderType)->first();
if (!$folder) return;
$messageId = $imapMessage->messageId() ?? uniqid('msg_');
$existing = EmailMessage::where('message_id', $messageId)->first();
if ($existing) return;
$from = $imapMessage->from();
$to = $imapMessage->to();
$message = EmailMessage::create([
'email_folder_id' => $folder->id,
'message_id' => $messageId,
'subject' => $imapMessage->subject() ?? '(senza oggetto)',
'from_name' => $from?->name() ?? '',
'from_email' => $from?->email() ?? '',
'to_email' => count($to) > 0 ? $to[0]->email() : '',
'body_text' => $imapMessage->text() ?? '',
'body_html' => $imapMessage->html() ?? '',
'is_read' => $imapMessage->isSeen() ?? false,
'is_starred' => $imapMessage->isFlagged() ?? false,
'is_sent' => $folderType === 'sent',
'received_at' => $imapMessage->date() ?? now(),
'imap_uid' => $imapMessage->uid() ?? null,
]);
$attachments = $imapMessage->attachments();
if ($attachments && count($attachments) > 0) {
foreach ($attachments as $attachment) {
$filename = $attachment->filename() ?? 'attachment';
$contents = $attachment->contents();
$path = 'email-attachments/' . Str::uuid() . '_' . $filename;
Storage::put($path, $contents);
EmailAttachment::create([
'email_message_id' => $message->id,
'filename' => $filename,
'mime_type' => $attachment->contentType() ?? 'application/octet-stream',
'size' => strlen($contents),
'file_path' => $path,
]);
}
}
}
private function ensureFoldersExist()
{
$systemFolders = EmailFolder::getSystemFolders();
foreach ($systemFolders as $folder) {
EmailFolder::updateOrCreate(
['type' => $folder['type']],
$folder
);
}
}
}