diff --git a/app/Services/Support/Gmail/GmailConnector.php b/app/Services/Support/Gmail/GmailConnector.php index 4129a89e8..cfaece62c 100644 --- a/app/Services/Support/Gmail/GmailConnector.php +++ b/app/Services/Support/Gmail/GmailConnector.php @@ -5,7 +5,7 @@ interface GmailConnector { /** - * @return array{messages: GmailMessage[], next_history_id: ?string} + * @return array{messages: GmailMessage[], next_history_id: ?string, warnings: string[]} */ public function fetchNewMessages( string $mailbox, diff --git a/app/Services/Support/Gmail/GmailIngestService.php b/app/Services/Support/Gmail/GmailIngestService.php index aeda35d01..d6bb45206 100644 --- a/app/Services/Support/Gmail/GmailIngestService.php +++ b/app/Services/Support/Gmail/GmailIngestService.php @@ -19,12 +19,12 @@ public function __construct( } /** - * @return array{ingested: int, duplicates: int, cursor_updated: bool} + * @return array{ingested: int, duplicates: int, cursor_updated: bool, warnings: string[]} */ public function pollAndIngest(int $max = 25): array { if (!config('support_gmail.enabled')) { - return ['ingested' => 0, 'duplicates' => 0, 'cursor_updated' => false]; + return ['ingested' => 0, 'duplicates' => 0, 'cursor_updated' => false, 'warnings' => []]; } $lockName = (string) config('support_gmail.lock.name', 'support:gmail:poll'); @@ -34,7 +34,7 @@ public function pollAndIngest(int $max = 25): array // to avoid double-ingesting on multiple nodes silently. $lock = Cache::lock($lockName, $ttlSeconds); if (!$lock->get()) { - return ['ingested' => 0, 'duplicates' => 0, 'cursor_updated' => false]; + return ['ingested' => 0, 'duplicates' => 0, 'cursor_updated' => false, 'warnings' => []]; } $mailbox = (string) config('support_gmail.user', 'me'); @@ -99,6 +99,7 @@ public function pollAndIngest(int $max = 25): array 'ingested' => $ingested, 'duplicates' => $duplicates, 'cursor_updated' => true, + 'warnings' => $result['warnings'] ?? [], ]; } finally { optional($lock)->release(); diff --git a/app/Services/Support/Gmail/GoogleGmailConnector.php b/app/Services/Support/Gmail/GoogleGmailConnector.php index 59db84c8f..a1107bbfe 100644 --- a/app/Services/Support/Gmail/GoogleGmailConnector.php +++ b/app/Services/Support/Gmail/GoogleGmailConnector.php @@ -4,6 +4,7 @@ use Google\Client as GoogleClient; use Google\Service\Gmail as GmailService; +use Google\Service\Gmail\Label as GoogleLabel; use Google\Service\Gmail\Message as GoogleMessage; use Illuminate\Support\Str; @@ -46,9 +47,18 @@ public function fetchNewMessages( $this->ensureValidToken(); $q = trim($query); + $labelId = null; + $warnings = []; if ($label) { - // Label scoping is done via labelIds, but keep query readable too. - $q = trim($q.' label:'.Str::of($label)->replace(' ', '-')); + // Label filtering is optional. If the label doesn't exist, we ingest without label scoping + // rather than failing the whole poll. + $labelId = $this->resolveLabelIdOrNull($mailbox, $label); + if ($labelId === null && trim((string) $label) !== '') { + $warnings[] = sprintf( + 'Configured Gmail label "%s" was not found; polling without label filter.', + trim((string) $label), + ); + } } $params = [ @@ -56,8 +66,8 @@ public function fetchNewMessages( 'maxResults' => $max, ]; - if ($label) { - $params['labelIds'] = [$label]; + if ($labelId) { + $params['labelIds'] = [$labelId]; } // V1: we use search-based ingestion; historyId is only used as a stored cursor. @@ -81,9 +91,34 @@ public function fetchNewMessages( return [ 'messages' => $messages, 'next_history_id' => $nextHistoryId, + 'warnings' => $warnings, ]; } + private function resolveLabelIdOrNull(string $mailbox, string $label): ?string + { + $label = trim($label); + if ($label === '') { + return null; + } + + // If user already provided a label ID (usually "Label_..."), use it directly. + if (Str::startsWith($label, 'Label_')) { + return $label; + } + + $labels = $this->gmail->users_labels->listUsersLabels($mailbox)->getLabels() ?? []; + + /** @var GoogleLabel $l */ + foreach ($labels as $l) { + if ($l->getId() === $label || $l->getName() === $label) { + return (string) $l->getId(); + } + } + + return null; + } + private function ensureValidToken(): void { $token = $this->client->getAccessToken(); diff --git a/app/Services/Support/Gmail/NullGmailConnector.php b/app/Services/Support/Gmail/NullGmailConnector.php index 641648cf3..adf3aabea 100644 --- a/app/Services/Support/Gmail/NullGmailConnector.php +++ b/app/Services/Support/Gmail/NullGmailConnector.php @@ -14,6 +14,7 @@ public function fetchNewMessages( return [ 'messages' => [], 'next_history_id' => $sinceHistoryId, + 'warnings' => [], ]; } } diff --git a/tests/Unit/Support/GmailIngestServiceTest.php b/tests/Unit/Support/GmailIngestServiceTest.php index 2f68e2ab4..275c74253 100644 --- a/tests/Unit/Support/GmailIngestServiceTest.php +++ b/tests/Unit/Support/GmailIngestServiceTest.php @@ -40,6 +40,7 @@ public function fetchNewMessages( new GmailMessage('m2', 't2', 'Subj 2', 'sender2@example.com', "Hello 2"), ], 'next_history_id' => '123', + 'warnings' => [], ]; } }; @@ -54,6 +55,7 @@ public function fetchNewMessages( $this->assertSame(2, $res['ingested']); $this->assertSame(1, $res['duplicates']); + $this->assertSame([], $res['warnings']); $cursor = SupportGmailCursor::query()->where('mailbox', 'me')->where('label', 'Support-AI')->first(); $this->assertNotNull($cursor); @@ -89,9 +91,50 @@ public function fetchNewMessages( ); $res = $svc->pollAndIngest(25); - $this->assertSame(['ingested' => 0, 'duplicates' => 0, 'cursor_updated' => false], $res); + $this->assertSame(['ingested' => 0, 'duplicates' => 0, 'cursor_updated' => false, 'warnings' => []], $res); $lock->release(); } + + public function test_poll_passes_through_connector_warnings(): void + { + config()->set('support_gmail.enabled', true); + config()->set('support_gmail.lock.name', 'test:support:gmail:poll:warnings'); + config()->set('support_gmail.lock.ttl_seconds', 30); + config()->set('support_gmail.user', 'me'); + config()->set('support_gmail.label', 'Missing-Label'); + config()->set('support_gmail.query', 'newer_than:7d'); + + $fakeConnector = new class implements GmailConnector { + public function fetchNewMessages( + string $mailbox, + ?string $label, + string $query, + ?string $sinceHistoryId, + int $max = 25, + ): array { + return [ + 'messages' => [], + 'next_history_id' => null, + 'warnings' => [ + 'Configured Gmail label "Missing-Label" was not found; polling without label filter.', + ], + ]; + } + }; + + $svc = new GmailIngestService( + connector: $fakeConnector, + intake: app(CaseIntakeService::class), + logger: app(SupportActionLogger::class), + ); + + $res = $svc->pollAndIngest(25); + + $this->assertSame( + ['Configured Gmail label "Missing-Label" was not found; polling without label filter.'], + $res['warnings'], + ); + } }