0, 'updated' => 0, 'errors' => 0, 'messages' => []]; try { $config = $this->configMapper->findByUserId($userId); } catch (DoesNotExistException $e) { $stats['messages'][] = 'No configuration found. Please save your settings first.'; return $stats; } if (!$config->isEnabled()) { $stats['messages'][] = 'Sync is disabled.'; return $stats; } $mailAccountId = $config->getMailAccountId(); $calendarUri = $config->getCalendarUri(); $autoAccept = $config->isAutoAccept(); if ($mailAccountId === null || $calendarUri === null) { $stats['messages'][] = 'Incomplete configuration — please select both a mail account and calendar.'; return $stats; } $this->logger->info('Starting mail-calendar sync', [ 'userId' => $userId, 'mailAccountId' => $mailAccountId, 'calendarUri' => $calendarUri, ]); // Find recent iMip messages try { $messages = $this->mailService->findRecentImipMessages($userId, $mailAccountId); $stats['messages'][] = 'Found ' . count($messages) . ' unprocessed iMIP message(s) in last 7 days.'; } catch (\Throwable $e) { $stats['errors']++; $stats['messages'][] = 'Failed to query mail: ' . $e->getMessage(); $this->logger->error('Failed to find iMIP messages', [ 'userId' => $userId, 'exception' => $e, ]); return $stats; } foreach ($messages as $message) { $stats['processed']++; $messageId = $message['messageId']; try { // Fetch ICS data from the actual email via IMAP (READ-ONLY) $icsResults = $this->mailService->fetchIcsFromMessage( $mailAccountId, $userId, $message['mailboxId'], $message['uid'], ); if (empty($icsResults)) { $stats['messages'][] = "No ICS data found in message: {$message['subject']}"; // Still mark as processed so we don't retry if ($messageId !== '') { $this->processedMapper->markProcessed($userId, $mailAccountId, $messageId); } continue; } foreach ($icsResults as $icsResult) { $this->processIcs( $userId, $icsResult['ics'], $icsResult['method'], $icsResult['from'], $calendarUri, $autoAccept, $stats, ); } // Mark message as processed if ($messageId !== '') { $this->processedMapper->markProcessed($userId, $mailAccountId, $messageId); } } catch (\Throwable $e) { $stats['errors']++; $stats['messages'][] = "Error processing '{$message['subject']}': " . $e->getMessage(); $this->logger->error('Error processing message', [ 'messageId' => $messageId, 'userId' => $userId, 'exception' => $e, ]); } } $this->logger->info('Mail-calendar sync completed', [ 'userId' => $userId, 'stats' => $stats, ]); return $stats; } /** * Process a single ICS payload. */ private function processIcs( string $userId, string $icsData, string $method, string $fromEmail, string $calendarUri, bool $autoAccept, array &$stats, ): void { try { $vcalendar = Reader::read($icsData); } catch (\Throwable $e) { $this->logger->warning('Failed to parse ICS data', ['exception' => $e]); $stats['errors']++; $stats['messages'][] = 'Failed to parse ICS: ' . $e->getMessage(); return; } $vevent = $vcalendar->VEVENT ?? null; $summary = $vevent ? (string)($vevent->SUMMARY ?? 'Unknown') : 'Unknown'; $uid = $vevent ? (string)($vevent->UID ?? '') : ''; $success = match ($method) { 'REPLY' => $this->calendarService->processReply($userId, $vcalendar, $fromEmail), 'REQUEST' => $this->calendarService->processRequest( $userId, $vcalendar, $fromEmail, $calendarUri, $autoAccept ), 'CANCEL' => $this->calendarService->processCancel($userId, $vcalendar, $fromEmail), default => false, }; if ($success) { $stats['updated']++; $stats['messages'][] = "{$method}: Updated '{$summary}' from {$fromEmail}"; } else { $stats['messages'][] = "{$method}: No action for '{$summary}' (event may not exist in calendar)"; } } /** * Run sync for all enabled users. */ public function syncAll(): array { $configs = $this->configMapper->findAllEnabled(); $results = []; foreach ($configs as $config) { $userId = $config->getUserId(); try { $results[$userId] = $this->syncForUser($userId); } catch (\Throwable $e) { $this->logger->error('Sync failed for user', [ 'userId' => $userId, 'exception' => $e, ]); $results[$userId] = [ 'processed' => 0, 'updated' => 0, 'errors' => 1, 'messages' => ['Sync failed: ' . $e->getMessage()], ]; } } return $results; } }