Skip to content
Merged

Dev #3495

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/Services/Support/Gmail/GmailConnector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 4 additions & 3 deletions app/Services/Support/Gmail/GmailIngestService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand Down Expand Up @@ -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();
Expand Down
43 changes: 39 additions & 4 deletions app/Services/Support/Gmail/GoogleGmailConnector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -46,18 +47,27 @@ 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 = [
'q' => $q,
'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.
Expand All @@ -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();
Expand Down
1 change: 1 addition & 0 deletions app/Services/Support/Gmail/NullGmailConnector.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public function fetchNewMessages(
return [
'messages' => [],
'next_history_id' => $sinceHistoryId,
'warnings' => [],
];
}
}
Expand Down
45 changes: 44 additions & 1 deletion tests/Unit/Support/GmailIngestServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public function fetchNewMessages(
new GmailMessage('m2', 't2', 'Subj 2', 'sender2@example.com', "Hello 2"),
],
'next_history_id' => '123',
'warnings' => [],
];
}
};
Expand All @@ -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);
Expand Down Expand Up @@ -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'],
);
}
}

Loading